diff --git a/EtwPerformanceProfiler/AL App Objects/PageExt.Session List.al b/EtwPerformanceProfiler/AL App Objects/PageExt.Session List.al new file mode 100644 index 0000000..1691e32 --- /dev/null +++ b/EtwPerformanceProfiler/AL App Objects/PageExt.Session List.al @@ -0,0 +1,44 @@ +pageextension 50145 SessionListPerformanceProfiler extends "Session List" +{ + actions + { + addafter("Debug Next Session") + { + action("Performance Profiler") + { + Caption = 'Performance Profiler'; + Image = Capacity; + Promoted = true; + PromotedCategory = Category4; + PromotedIsBig = true; + + trigger OnAction() + var + PerfProfiler: Page "Performance Profiler"; + begin + if "Session ID" > 0 then + PerfProfiler.SetTargetSessionID("Session ID"); + + PerfProfiler.RUN; + end; + } + action("Terminate Session") + { + Caption = 'Terminate Session'; + Image = Delete; + Promoted = true; + PromotedCategory = Category4; + PromotedIsBig = true; + Ellipsis = true; + + trigger OnAction() + var + TerminateConfirmTxt: Label 'Terminate selected session?'; + begin + if Confirm(TerminateConfirmTxt, true) then + StopSession("Session ID"); + end; + } + } + } +} \ No newline at end of file diff --git a/EtwPerformanceProfiler/AL App Objects/PerformanceProfiler.Page.al b/EtwPerformanceProfiler/AL App Objects/PerformanceProfiler.Page.al new file mode 100644 index 0000000..90d02cb --- /dev/null +++ b/EtwPerformanceProfiler/AL App Objects/PerformanceProfiler.Page.al @@ -0,0 +1,371 @@ +page 50145 "Performance Profiler" +{ + DeleteAllowed = false; + InsertAllowed = false; + ModifyAllowed = true; + PageType = List; + PromotedActionCategories = 'New,Process,Report,Archive'; + SourceTable = "Performance Profiler Events"; + UsageCategory = Administration; + ApplicationArea = All; + LinksAllowed = false; + + layout + { + area(content) + { + group(Configuration) + { + Caption = 'Configuration'; + field("Target Session ID"; TargetSessionID) + { + Editable = TargetSessionIDEditable; + Lookup = true; + TableRelation = "Active Session"."Session ID"; + + trigger OnValidate() + begin + if (TargetSessionID <> MultipleSessionsId) then + Rec.SetFilter("Session ID", '=%1', TargetSessionID); + end; + } + field(Threshold; Threshold) + { + } + field(ProfileMultipleSessions; ProfileMultipleSessions) + { + Caption = 'Profile multiple Sessions'; + + trigger OnValidate() + begin + if (ProfileMultipleSessions) then begin + TargetSessionID := MultipleSessionsId; + TargetSessionIDEditable := false + end else begin + TargetSessionID := SessionId(); + TargetSessionIDEditable := true + end; + end; + } + } + repeater("Call Tree") + { + Editable = false; + IndentationColumn = Indentation; + ShowAsTree = true; + field("Object Type"; "Object Type") + { + Editable = false; + Enabled = false; + } + field("Object ID"; "Object ID") + { + } + field("Line No"; "Line No") + { + } + field(Statement; Statement) + { + } + field(Duration; Duration) + { + Caption = 'Duration (ms)'; + Style = Attention; + } + field(MinDuration; MinDuration) + { + } + field(MaxDuration; MaxDuration) + { + } + field(LastActive; LastActive) + { + } + field(HitCount; HitCount) + { + } + field(Id; Id) + { + } + field("Session ID"; "Session ID") + { + } + } + group(Control17) + { + ShowCaption = false; + field(Total; Total) + { + Caption = 'Total Time (ms):'; + Editable = false; + Enabled = false; + Style = Strong; + StyleExpr = TRUE; + } + } + } + } + + actions + { + area(processing) + { + action(Start) + { + Enabled = NOT ProfilerStarted; + Image = Start; + Promoted = true; + PromotedCategory = Process; + PromotedIsBig = true; + PromotedOnly = true; + + trigger OnAction() + begin + Clear(Rec); + + if (MultipleSessionsId = TargetSessionID) then begin + Clear(Rec); + Rec.Init; + Rec.DeleteAll; + end else begin + Rec.SetFilter("Session ID", '=%1', TargetSessionID); + Rec.DeleteAll; + end; + + PerformanceProfiler.Start(TargetSessionID, Threshold); + + ProfilerStarted := true; + end; + } + action(Stop) + { + Enabled = ProfilerStarted; + Image = Stop; + Promoted = true; + PromotedCategory = Process; + PromotedIsBig = true; + PromotedOnly = true; + + trigger OnAction() + begin + PerformanceProfiler.Stop; + ProfilerStarted := false; + + WaitForDataToBeCollected; + + CopyEventsFromProfilerToTable; + if (TargetSessionID <> MultipleSessionsId) then + Rec.SetFilter("Session ID", '=%1', TargetSessionID); + end; + } + action("Analyze ETL File") + { + Enabled = NOT ProfilerStarted; + Image = AnalysisView; + Promoted = true; + PromotedCategory = Process; + PromotedIsBig = true; + PromotedOnly = true; + + trigger OnAction() + var + FileManagement: Codeunit "File Management"; + ETLFileName: Text; + begin + ETLFileName := FileManagement.OpenFileDialog('Analyze ETL File', '', 'Trace Files (*.etl)|*.etl'); + + if (ETLFileName <> '') then begin + PerformanceProfiler.AnalyzeETLFile(ETLFileName, Threshold); + + Clear(Rec); + Rec.Init; + Rec.DeleteAll; + + CopyEventsFromProfilerToTable; + end; + end; + } + action("Clear Codeunit 1 calls") + { + Image = ClearFilter; + Promoted = true; + PromotedCategory = Process; + PromotedIsBig = true; + PromotedOnly = true; + + trigger OnAction() + var + codeUnit1Call: Boolean; + begin + codeUnit1Call := false; + + FindFirst; + + repeat + if (Indentation = 0) then begin + if (("Object Type" = "Object Type"::Codeunit) and ("Object ID" = 1)) then + codeUnit1Call := true + else + codeUnit1Call := false; + end; + + if (codeUnit1Call) then + Delete; + until Next = 0; + end; + } + action("Get Statement") + { + Image = Comment; + Promoted = true; + PromotedCategory = Process; + PromotedIsBig = true; + PromotedOnly = true; + + trigger OnAction() + var + InStream: InStream; + StatementBigTxt: BigText; + StatementTxt: Text; + begin + CalcFields(FullStatement); + + FullStatement.CreateInStream(InStream); + StatementBigTxt.Read(InStream); + + StatementBigTxt.GetSubText(StatementTxt, 1, StatementBigTxt.Length); + + Message(StatementTxt); + end; + } + action("Copy To Archive") + { + Image = CopyWorksheet; + Promoted = true; + PromotedCategory = Category4; + PromotedIsBig = true; + PromotedOnly = true; + + trigger OnAction() + begin + CopyToArchive("Session ID"); + end; + } + action(Archive) + { + Image = Archive; + Promoted = true; + PromotedCategory = Category4; + PromotedIsBig = true; + PromotedOnly = true; + RunObject = Page "Performance Profiler Archive"; + } + } + } + + trigger OnAfterGetCurrRecord() + begin + CalcFields(Total); + end; + + trigger OnInit() + begin + MaxStatementLength := 250; + Threshold := 0; + TargetSessionID := SessionId(); + TargetSessionIDEditable := true; + MultipleSessionsId := -1; + end; + + trigger OnOpenPage() + begin + // Assume that profiler hasn't been started. + // This assumption might be wrong. + ProfilerStarted := false; + if (TargetSessionID <> MultipleSessionsId) then + Rec.SetFilter("Session ID", '=%1', TargetSessionID); + + if (Rec.IsEmpty) then begin + Rec."Session ID" := TargetSessionID; + Rec.Insert; + end; + + PerformanceProfiler := PerformanceProfiler.EtwPerformanceProfiler(); + end; + + var + ProgressDialog: Dialog; + PerformanceProfiler: DotNet EtwPerformanceProfiler; + [InDataSet] + ProfilerStarted: Boolean; + TargetSessionIDEditable: Boolean; + [InDataSet] + TargetSessionID: Integer; + PleaseWaitCollectingDataTxt: Label 'Collecting performance data \\Please wait #1'; + Threshold: Integer; + MultipleSessionsId: Integer; + MaxStatementLength: Integer; + ProfileMultipleSessions: Boolean; + + local procedure WaitForDataToBeCollected() + var + SecondsToWait: Integer; + begin + SecondsToWait := 3; + ProgressDialog.Open(PleaseWaitCollectingDataTxt); + while SecondsToWait > 0 do begin + ProgressDialog.Update(1, StrSubstNo('%1 s', SecondsToWait)); + Sleep(1000); + SecondsToWait -= 1; + end; + ProgressDialog.Close; + end; + + local procedure CopyEventsFromProfilerToTable() + var + OutStream: OutStream; + I: Integer; + StatementTxt: Text; + StatementBigTxt: BigText; + begin + I := 1; + + while (PerformanceProfiler.CallTreeMoveNext) do begin + Clear(Rec); + Rec.Init; + Rec.Id := I; + Rec."Session ID" := PerformanceProfiler.CallTreeCurrentStatementSessionId; + Rec.Indentation := PerformanceProfiler.CallTreeCurrentStatementIndentation; + Rec."Object Type" := PerformanceProfiler.CallTreeCurrentStatementOwningObjectType; + Rec."Object ID" := PerformanceProfiler.CallTreeCurrentStatementOwningObjectId; + Rec."Line No" := PerformanceProfiler.CallTreeCurrentStatementLineNo; + + StatementTxt := PerformanceProfiler.CallTreeCurrentStatement; + if (StrLen(StatementTxt) > MaxStatementLength) then begin + Statement := CopyStr(StatementTxt, 1, MaxStatementLength); + end else begin + Rec.Statement := StatementTxt; + end; + Clear(StatementBigTxt); + StatementBigTxt.AddText(StatementTxt); + FullStatement.CreateOutStream(OutStream); + StatementBigTxt.Write(OutStream); + + Rec.Duration := PerformanceProfiler.CallTreeCurrentStatementDurationMs; + Rec.MinDuration := PerformanceProfiler.CallTreeCurrentStatementMinDurationMs; + Rec.MaxDuration := PerformanceProfiler.CallTreeCurrentStatementMaxDurationMs; + Rec.LastActive := PerformanceProfiler.CallTreeCurrentStatementLastActiveMs; + Rec.HitCount := PerformanceProfiler.CallTreeCurrentStatementHitCount; + Rec.Insert; + + I += 1; + end; + end; + + [Scope('Internal')] + procedure SetTargetSessionID(NewTargetSessionID: Integer) + begin + TargetSessionID := NewTargetSessionID; + end; +} + diff --git a/EtwPerformanceProfiler/AL App Objects/PerformanceProfilerArchive.Page.al b/EtwPerformanceProfiler/AL App Objects/PerformanceProfilerArchive.Page.al new file mode 100644 index 0000000..fd06c7a --- /dev/null +++ b/EtwPerformanceProfiler/AL App Objects/PerformanceProfilerArchive.Page.al @@ -0,0 +1,142 @@ +page 50146 "Performance Profiler Archive" +{ + DeleteAllowed = false; + InsertAllowed = false; + ModifyAllowed = false; + PageType = Worksheet; + SourceTable = "Performance Profiler Archive"; + + layout + { + area(content) + { + group(Configuration) + { + Caption = 'Configuration'; + field("Target Session Code"; TargetSessionCode) + { + Lookup = true; + TableRelation = "Performance Session Archived"; + + trigger OnValidate() + begin + Rec.SetRange("Session Code", TargetSessionCode); + CurrPage.Update; + end; + } + field("Duration Threshold"; DurationThreshold) + { + + trigger OnValidate() + begin + SetFilter(Duration, '%1..', DurationThreshold); + CurrPage.Update; + end; + } + field("Base Duration"; BaseDuration) + { + + trigger OnValidate() + begin + CurrPage.Update; + end; + } + } + repeater("Call Tree") + { + Editable = false; + IndentationColumn = Indentation; + ShowAsTree = true; + field("Object Type"; "Object Type") + { + Editable = false; + Enabled = false; + } + field("Object ID"; "Object ID") + { + } + field("Line No"; "Line No") + { + } + field(Statement; Statement) + { + } + field(Duration; Duration) + { + Caption = 'Duration (ms)'; + Style = Attention; + } + field(MinDuration; MinDuration) + { + } + field(MaxDuration; MaxDuration) + { + } + field(LastActive; LastActive) + { + } + field(HitCount; HitCount) + { + } + field(Id; Id) + { + } + field("Session Code"; "Session Code") + { + Visible = false; + } + field(Indentation; Indentation) + { + } + field(Ratio; GetRatio) + { + Caption = 'Ratio %'; + DecimalPlaces = 3 : 3; + } + } + } + } + + actions + { + area(processing) + { + action("Calculate Ratio") + { + Image = CalculateHierarchy; + Promoted = true; + PromotedCategory = Process; + PromotedIsBig = true; + + trigger OnAction() + begin + BaseDuration := Duration; + CurrPage.Update; + end; + } + } + } + + trigger OnInit() + begin + if FindFirst then + TargetSessionCode := "Session Code"; + end; + + trigger OnOpenPage() + begin + Rec.SetRange("Session Code", TargetSessionCode); + end; + + var + TargetSessionCode: Code[20]; + BaseDuration: Decimal; + DurationThreshold: Decimal; + + local procedure GetRatio(): Decimal + begin + if BaseDuration <> 0 then + exit(Duration / BaseDuration * 100); + end; +} + diff --git a/EtwPerformanceProfiler/AL App Objects/PerformanceProfilerArchive.Table.al b/EtwPerformanceProfiler/AL App Objects/PerformanceProfilerArchive.Table.al new file mode 100644 index 0000000..4e4d9bc --- /dev/null +++ b/EtwPerformanceProfiler/AL App Objects/PerformanceProfilerArchive.Table.al @@ -0,0 +1,72 @@ +table 50146 "Performance Profiler Archive" +{ + DataPerCompany = false; + + fields + { + field(1; Id; Integer) + { + } + field(3; Indentation; Integer) + { + } + field(4; "Object Type"; Option) + { + Caption = 'Object Type'; + OptionCaption = 'TableData,Table,Form,Report,Dataport,Codeunit,XMLport,MenuSuite,Page,Query,System,FieldNumber'; + OptionMembers = TableData,"Table",Form,"Report",Dataport,"Codeunit","XMLport",MenuSuite,"Page","Query",System,FieldNumber; + } + field(5; "Object ID"; Integer) + { + Caption = 'Object ID'; + TableRelation = Object.ID WHERE(Type = FIELD("Object Type")); + //This property is currently not supported + //TestTableRelation = false; + } + field(6; "Line No"; Integer) + { + } + field(7; Statement; Text[250]) + { + } + field(8; Duration; Decimal) + { + } + field(9; MinDuration; Decimal) + { + } + field(10; MaxDuration; Decimal) + { + } + field(11; LastActive; Decimal) + { + } + field(12; HitCount; Integer) + { + } + field(13; Total; Decimal) + { + CalcFormula = Sum ("Performance Profiler Events".Duration WHERE(Indentation = CONST(0))); + FieldClass = FlowField; + } + field(14; FullStatement; BLOB) + { + } + field(20; "Session Code"; Code[20]) + { + } + } + + keys + { + key(Key1; "Session Code", Id) + { + Clustered = true; + } + } + + fieldgroups + { + } +} + diff --git a/EtwPerformanceProfiler/AL App Objects/PerformanceProfilerEvents.Table.al b/EtwPerformanceProfiler/AL App Objects/PerformanceProfilerEvents.Table.al new file mode 100644 index 0000000..5aaa315 --- /dev/null +++ b/EtwPerformanceProfiler/AL App Objects/PerformanceProfilerEvents.Table.al @@ -0,0 +1,131 @@ +table 50145 "Performance Profiler Events" +{ + DataPerCompany = false; + + fields + { + field(1; Id; Integer) + { + } + field(2; "Session ID"; Integer) + { + } + field(3; Indentation; Integer) + { + } + field(4; "Object Type"; Option) + { + Caption = 'Object Type'; + OptionCaption = 'TableData,Table,Form,Report,Dataport,Codeunit,XMLport,MenuSuite,Page,Query,System,FieldNumber'; + OptionMembers = TableData,"Table",Form,"Report",Dataport,"Codeunit","XMLport",MenuSuite,"Page","Query",System,FieldNumber; + } + field(5; "Object ID"; Integer) + { + Caption = 'Object ID'; + TableRelation = Object.ID WHERE(Type = FIELD("Object Type")); + //This property is currently not supported + //TestTableRelation = false; + } + field(6; "Line No"; Integer) + { + } + field(7; Statement; Text[250]) + { + } + field(8; Duration; Decimal) + { + } + field(9; MinDuration; Decimal) + { + } + field(10; MaxDuration; Decimal) + { + } + field(11; LastActive; Decimal) + { + } + field(12; HitCount; Integer) + { + } + field(13; Total; Decimal) + { + CalcFormula = Sum ("Performance Profiler Events".Duration WHERE(Indentation = CONST(0))); + FieldClass = FlowField; + } + field(14; FullStatement; BLOB) + { + } + } + + keys + { + key(Key1; Id, "Session ID") + { + Clustered = true; + } + key(Key2; Indentation) + { + } + } + + fieldgroups + { + } + + var + DeleteArchiveQst: Label 'Existing lines of the archived session %1 will be deleted. Do you want to continue?'; + CopyedToArchMsg: Label 'Data is archived with Session Code %1.'; + + [Scope('Internal')] + procedure CopyToArchive(CurrSessionId: Integer) + var + ProfilerArchive: Record "Performance Profiler Archive"; + ProfilerEvent: Record "Performance Profiler Events"; + SessionCode: Code[20]; + begin + if FindSessionCode(SessionCode) then begin + ProfilerEvent.Reset; + ProfilerEvent.SetRange("Session ID", CurrSessionId); + if ProfilerEvent.FindSet then begin + repeat + ProfilerArchive.Init; + ProfilerArchive.TransferFields(ProfilerEvent); + ProfilerArchive."Session Code" := SessionCode; + ProfilerArchive.Insert; + until ProfilerEvent.Next = 0; + Message(CopyedToArchMsg, SessionCode); + end; + end; + end; + + local procedure FindSessionCode(var SessionCode: Code[20]): Boolean + var + ProfilerSessionArch: Record "Performance Session Archived"; + PerfSessionArchived: Page "Performance Session Archived"; + begin + PerfSessionArchived.LookupMode(true); + if PerfSessionArchived.RunModal = ACTION::LookupOK then begin + PerfSessionArchived.GetRecord(ProfilerSessionArch); + ProfilerSessionArch.TestField(Code); + SessionCode := ProfilerSessionArch.Code; + + exit(RemoveExistingArchLines(SessionCode)); + end; + exit(false); + end; + + local procedure RemoveExistingArchLines(SessionCode: Code[20]): Boolean + var + ProfilerArchive: Record "Performance Profiler Archive"; + begin + ProfilerArchive.Reset; + ProfilerArchive.SetRange("Session Code", SessionCode); + if not ProfilerArchive.IsEmpty then begin + if not Confirm(DeleteArchiveQst, false, SessionCode) then + exit(false); + ProfilerArchive.DeleteAll; + end; + exit(true); + end; +} + diff --git a/EtwPerformanceProfiler/AL App Objects/PerformanceSessionArchived.Page.al b/EtwPerformanceProfiler/AL App Objects/PerformanceSessionArchived.Page.al new file mode 100644 index 0000000..6aa251c --- /dev/null +++ b/EtwPerformanceProfiler/AL App Objects/PerformanceSessionArchived.Page.al @@ -0,0 +1,26 @@ +page 50147 "Performance Session Archived" +{ + PageType = List; + SourceTable = "Performance Session Archived"; + + layout + { + area(content) + { + repeater(Group) + { + field("Code"; Code) + { + } + field(Description; Description) + { + } + } + } + } + + actions + { + } +} + diff --git a/EtwPerformanceProfiler/AL App Objects/PerformanceSessionArchived.Table.al b/EtwPerformanceProfiler/AL App Objects/PerformanceSessionArchived.Table.al new file mode 100644 index 0000000..a81a202 --- /dev/null +++ b/EtwPerformanceProfiler/AL App Objects/PerformanceSessionArchived.Table.al @@ -0,0 +1,36 @@ +table 50147 "Performance Session Archived" +{ + DataCaptionFields = "Code", Description; + LookupPageID = "Performance Session Archived"; + + fields + { + field(1; "Code"; Code[20]) + { + } + field(2; Description; Text[250]) + { + } + } + + keys + { + key(Key1; "Code") + { + Clustered = true; + } + } + + fieldgroups + { + } + + trigger OnDelete() + var + PerformanceProfilerArchive: Record "Performance Profiler Archive"; + begin + PerformanceProfilerArchive.SetRange("Session Code", Code); + PerformanceProfilerArchive.DeleteAll; + end; +} + diff --git a/EtwPerformanceProfiler/AL App Objects/app.json b/EtwPerformanceProfiler/AL App Objects/app.json new file mode 100644 index 0000000..9e79594 --- /dev/null +++ b/EtwPerformanceProfiler/AL App Objects/app.json @@ -0,0 +1,26 @@ +{ + "id": "4e6bd02f-26c3-4019-9d3e-936b2254bd6f", + "name": "ETW Performance Profiler", + "publisher": "Default Publisher", + "version": "1.0.0.0", + "brief": "", + "description": "Tool for profiling Dynamics NAV/BC Application code. Consumes NAV Execution Events from ETW. To begin, open Session List via debugger, choose session and open Performance Profiler.", + "privacyStatement": "", + "EULA": "", + "help": "", + "url": "https://github.com/wortho/EtwPerformanceProfiler", + "logo": "", + "dependencies": [], + "screenshots": [], + "platform": "14.0.0.0", + "application": "14.0.0.0", + "idRanges": [ + { + "from": 50145, + "to": 50149 + } + ], + "target": "Internal", + "showMyCode": true, + "runtime": "3.0" +} \ No newline at end of file diff --git a/EtwPerformanceProfiler/AL App Objects/dotnet.al b/EtwPerformanceProfiler/AL App Objects/dotnet.al new file mode 100644 index 0000000..25afd17 --- /dev/null +++ b/EtwPerformanceProfiler/AL App Objects/dotnet.al @@ -0,0 +1,13 @@ +dotnet +{ + assembly("EtwPerformanceProfiler") + { + Version = '2.0.0.0'; + Culture = 'neutral'; + PublicKeyToken = 'null'; + + type("EtwPerformanceProfiler.EtwPerformanceProfiler"; "EtwPerformanceProfiler") + { + } + } +}