unit HeapTrcView; {$mode objfpc}{$H+} interface uses Classes, SysUtils, XMLConf, DOM, contnrs, Clipbrd, LCLProc, LCLType, LResources, Forms, Controls, Graphics, Dialogs, StdCtrls, ComCtrls, ExtCtrls, // LazUtils FileUtil, LazFileUtils, // IDEIntf LazIDEIntf, MenuIntf, ToolBarIntf, IDECommands, // LeakView LeakInfo; type TJumpProc = procedure (Sender: TObject; const SourceName: string; Line, Column: integer) of object; { THeapTrcViewForm } THeapTrcViewForm = class(TForm) btnUpdate: TButton; btnBrowse: TButton; btnClipboard: TButton; BtnResolve: TButton; chkUseRaw: TCheckBox; chkStayOnTop: TCheckBox; edtTrcFileName:TComboBox; lblTrcFile: TLabel; ctrlPanel: TPanel; memoSummary: TMemo; OpenDialog: TOpenDialog; splitter: TSplitter; trvTraceInfo: TTreeView; procedure btnClipboardClick(Sender: TObject); procedure BtnResolveClick(Sender: TObject); procedure btnUpdateClick(Sender: TObject); procedure btnBrowseClick(Sender: TObject); procedure chkStayOnTopChange(Sender: TObject); procedure chkUseRawChange(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure trvTraceInfoDblClick(Sender: TObject); private Finfo : TLeakInfo; fItems : TStackTraceList; procedure DoUpdateLeaks(FromClip: Boolean = False); procedure ItemsToTree; procedure ChangeTreeText; procedure ClearItems; procedure DoJump; function GetStackTraceText(trace: TStackTrace; useRaw: Boolean): string; function GetStackLineText(const Line: TStackLine; useRaw: Boolean): string; procedure SaveState(cfg: TXMLConfig); procedure LoadState(cfg: TXMLConfig); procedure AddFileToList(const FileName: AnsiString); protected procedure LazarusJump(Sender: TObject; const SourceFile: string; Line, Column: Integer); public destructor Destroy; override; public OnJumpProc : TJumpProc; //= procedure (Sender: TObject; const SourceName: string; Line: integer) of object; end; resourcestring StackTraceFormat = 'Leak: %d bytes x %d times'; // number of bytes leaked, leaks count StackTraceFormatSingle = 'Leak: %d bytes'; // number of bytes leaked StackLineFormatWithFile = '%s file: %s : %d; '; // stack addr, filename (no path), line number StackLineFormat = '%s'; // stack addr strTotalMemAlloc = 'Total Mem allocated: %d'; strLeakingMemSize = 'Leaking Mem Size: %d'; strLeakingBlocksCount = 'Leaking Blocks Count: %d'; // rsErrorParse = 'Error while parsing trace file'; rsDTimes = ' (%d times)'; rsLeakView = 'Leaks and Traces'; // slblTrace = '.trc file'; sbtnUpdate = 'Update'; sbtnClipBrd = 'Paste Clipboard'; sbtnResolve = 'Resolve'; schkRaw = 'Raw leak data'; schkTop = 'Stay on top'; sfrmCap = 'Leaks and Traces - HeapTrc and GDB backtrace output viewer'; sfrmSelectFileWithDebugInfo = 'Select file with debug info'; sfrmSelectTrcFile = 'Select file with trace log'; var HeapTrcViewForm: THeapTrcViewForm = nil; // JumpProc is the callback that is called everytime user double clicks // on the leak line. It's legal to pass nil, then LazarusIDE is used to peform a jump procedure ShowHeapTrcViewForm(JumpProc: TJumpProc = nil); procedure Register; implementation {$R *.lfm} procedure ShowHeapTrcViewForm(JumpProc: TJumpProc); begin if not Assigned(HeapTrcViewForm) then HeapTrcViewForm := THeapTrcViewForm.Create(Application); if Assigned(JumpProc) then HeapTrcViewForm.OnJumpProc := JumpProc else HeapTrcViewForm.OnJumpProc := @HeapTrcViewForm.LazarusJump; HeapTrcViewForm.Show; end; { THeapTrcViewForm } procedure THeapTrcViewForm.btnUpdateClick(Sender: TObject); begin DoUpdateLeaks; AddFileToList(edtTrcFileName.Text); end; procedure THeapTrcViewForm.btnClipboardClick(Sender: TObject); begin DoUpdateLeaks(True); end; procedure THeapTrcViewForm.BtnResolveClick(Sender: TObject); begin if Finfo = nil then exit; OpenDialog.FileName := ''; OpenDialog.Filter := ''; OpenDialog.Title := sfrmSelectFileWithDebugInfo; if not OpenDialog.Execute then Exit; Finfo.ResolveLeakInfo(OpenDialog.FileName, fItems); ChangeTreeText; end; procedure THeapTrcViewForm.btnBrowseClick(Sender: TObject); begin OpenDialog.FileName := ''; OpenDialog.Filter := slblTrace + '|*.trc'; OpenDialog.Title := sfrmSelectTrcFile; if not OpenDialog.Execute then Exit; edtTrcFileName.Text := OpenDialog.FileName; DoUpdateLeaks; AddFileToList(edtTrcFileName.Text); end; procedure THeapTrcViewForm.chkStayOnTopChange(Sender: TObject); begin if chkStayOnTop.Checked then Self.formStyle := fsStayOnTop else Self.formStyle := fsNormal; end; procedure THeapTrcViewForm.chkUseRawChange(Sender: TObject); begin ChangeTreeText; trvTraceInfo.Invalidate; end; var ConfigFileName : AnsiString = ''; function CreateXMLConfig: TXMLConfig; begin Result:=TXMLConfig.Create(nil); Result.RootName:='config'; if (ConfigFileName='') and Assigned(LazarusIDE) then ConfigFileName:=AppendPathDelim(LazarusIDE.GetPrimaryConfigPath)+'leakview.xml'; Result.FileName:=ConfigFileName; end; procedure THeapTrcViewForm.FormCreate(Sender: TObject); var cfg : TXMLConfig; begin Caption:=sfrmCap; lblTrcFile.Caption:=slblTrace; btnUpdate.Caption:=sbtnUpdate; btnClipboard.Caption:=sbtnClipBrd; BtnResolve.Caption:=sbtnResolve; chkUseRaw.Caption:=schkRaw; chkStayOnTop.Caption:=schkTop; fItems:=TStackTraceList.Create; try cfg:=CreateXMLConfig; try LoadState(cfg); finally cfg.Free; end; except end; end; procedure THeapTrcViewForm.FormDestroy(Sender: TObject); var cfg : TXMLConfig; begin ClearItems; fItems.Free; try cfg:=CreateXMLConfig; try SaveState(cfg); finally cfg.Free; end; except end; HeapTrcViewForm:=nil; end; procedure THeapTrcViewForm.trvTraceInfoDblClick(Sender: TObject); begin DoJump; end; //note: to range check performed procedure HexInt64ToStr(i64: Int64; var s: string; ofs: Integer); var i : Integer; j : Integer; const Hexes: array [0..$F] of char = ('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'); begin j := ofs + 15; for i := 0 to 7 do begin s[j] := Hexes[ i64 and $F ]; dec(j); s[j] := Hexes[ ((i64 and $F0) shr 4) and $F ]; dec(j); i64 := i64 shr 8; end; end; function GetHashString(trace: TStackTrace): string; var i : integer; sz : Integer; begin sz := 32 + trace.Count * 16; // 8 hex digits for Size + 8 hex digits for Size SetLength(Result, sz); HexInt64ToStr(trace.BlockSize, Result, 1); HexInt64ToStr(hash(trace.RawStackData), Result, 17); for i := 0 to trace.Count - 1 do if trace.lines[i].Addr <> 0 then HexInt64ToStr(trace.lines[i].Addr, Result, 33 + i * 16) else HexInt64ToStr(Hash(trace.lines[i].RawLineData), Result, 17 + i * 16); end; procedure THeapTrcViewForm.ItemsToTree; var i : Integer; j : Integer; trace : TStackTrace; nd : TTreeNode; hash : TFPObjectHashTable; hashed : TStackTrace; s : string; begin hash := TFPObjectHashTable.Create(false); try // removing duplicates for i := 0 to fItems.Count - 1 do begin trace := TStackTrace(fItems[i]); if trace = nil then continue; s := GetHashString(trace); hashed := TStackTrace(hash.Items[s]); if Assigned(hashed) then begin inc(hashed.LeakCount); fItems[i] := nil; // this call destroy on the old trace object end else hash.Add(s, trace) end; fItems.Pack; // filling the tree for i := 0 to fItems.Count - 1 do begin trace := TStackTrace(fItems[i]); nd := trvTraceInfo.Items.AddChildObject(nil, '+', trace); for j := 0 to trace.Count - 1 do begin trvTraceInfo.Items.AddChildObject(nd, '-', {%H-}Pointer(PtrInt(j))); end; end; // updating tree text ChangeTreeText; finally hash.free; end; end; procedure THeapTrcViewForm.ClearItems; var i : integer; begin fItems.Clear; end; procedure THeapTrcViewForm.DoUpdateLeaks(FromClip: Boolean = False); var data : TLeakStatus; txt: String; begin FreeAndNil(Finfo); trvTraceInfo.BeginUpdate; try ClearItems; trvTraceInfo.Items.Clear; if FromClip then begin txt := Clipboard.AsText; if txt = '' then exit; Finfo := AllocHeapTraceInfoFromText(txt); end else begin if (not FileExistsUTF8(edtTrcFileName.Text)) or FromClip then Exit; Finfo := AllocHeapTraceInfo(edtTrcFileName.Text); end; if Finfo.GetLeakInfo(data, fItems) then ItemsToTree else trvTraceInfo.Items.Add(nil, rsErrorParse); memoSummary.Clear; with memoSummary.Lines do begin Add( Format(strTotalMemAlloc, [data.TotalMem])); Add( Format(strLeakingMemSize, [data.LeakedMem])); Add( Format(strLeakingBlocksCount, [data.LeakCount])); end; finally trvTraceInfo.EndUpdate; end; if trvTraceInfo.Items.TopLvlCount = 1 then trvTraceInfo.Items.TopLvlItems[0].Expand(False); end; procedure THeapTrcViewForm.DoJump; var nd : TTreeNode; searchFile : string; idx : Integer; trace : TStackTrace; StackLine: TStackLine; begin if not Assigned(@OnJumpProc) then Exit; nd := trvTraceInfo.Selected; if not Assigned(nd) then Exit; if nd.Parent = nil then Exit; idx := Integer({%H-}PtrInt(nd.Data)); trace := TStackTrace(nd.Parent.Data); if not Assigned(trace) or (idx >= trace.Count) then Exit; searchFile := trace.Lines[idx].FileName; if searchFile = '' then Exit; StackLine:= trace.Lines[idx]; OnJumpProc(Self, searchFile, StackLine.LineNum, StackLine.Column); end; procedure THeapTrcViewForm.ChangeTreeText; var i, j : Integer; useRaw : Boolean; nd : TTreeNode; trace : TStackTrace; begin trvTraceInfo.Items.BeginUpdate; try useRaw := chkUseRaw.Checked; for i := 0 to trvTraceInfo. Items.Count - 1 do begin nd := TTreeNode(trvTraceInfo.Items[i]); if Assigned(nd.Parent) or not Assigned(nd.Data) then Continue; trace := TStackTrace(nd.Data); nd.Text := GetStackTraceText(trace, useRaw); for j := 0 to nd.Count - 1 do begin nd.Items[j].Text := GetStackLineText( trace.Lines[j], useRaw ); end; end; finally trvTraceInfo.Items.EndUpdate; end; end; function THeapTrcViewForm.GetStackTraceText(trace: TStackTrace; useRaw: boolean): string; begin if useRaw then begin Result := trace.RawStackData; if (Result <> '') and (trace.LeakCount > 1) then Result := Result + Format( rsDTimes, [trace.LeakCount]); end; if not useRaw or (Result = '') then begin if trace.LeakCount > 1 then Result := Format(StackTraceFormat, [trace.BlockSize, trace.LeakCount]) else Result := Format(StackTraceFormatSingle, [trace.BlockSize]); end; end; function THeapTrcViewForm.GetStackLineText(const Line: TStackLine; useRaw: boolean): string; begin if useRaw then Result := Line.RawLineData; if (not useRaw) or (Result = '') or ( (Pos(' ', Trim(Result)) < 1) and (Pos(':', Trim(Result)) < 1) and ( (copy(Trim(Result),1,1) = '$') or (copy(Trim(Result),1,2) = '0x') ) ) // Rawdata may be address only then with Line do if FileName <> '' then Result := Format(StackLineFormatWithFile, ['$'+IntToHex(Addr, sizeof(Pointer)*2), ExtractFileName(FileName), LineNum]) else Result := Format(StackLineFormat, ['$'+IntToHex(Addr, sizeof(Pointer)*2)]); end; procedure THeapTrcViewForm.SaveState(cfg:TXMLConfig); var b : TRect; i : Integer; begin cfg.SetValue('isStayOnTop',FormStyle=fsStayOnTop); b:=BoundsRect; cfg.OpenKey('bounds'); cfg.SetValue('left', b.Left); cfg.SetValue('top', b.Top); cfg.SetValue('right', b.Right); cfg.SetValue('bottom', b.Bottom); cfg.CloseKey; for i:=0 to edtTrcFileName.Items.Count-1 do cfg.SetValue(DOMString('path'+IntToStr(i)), UTF8Decode(edtTrcFileName.Items[i]) ); end; function PointInRect(p: TPoint; const r: TRect): Boolean; begin Result:=(p.X>=r.Left) and (p.X<=r.Right) and (p.y>=r.Top) and (p.y<=r.Bottom); end; procedure inAnyMonitor(var b: TRect); var m: TMonitor; mb: TRect; const MinOverLap = 40; begin m := Screen.MonitorFromRect(b); // Nearest Monitor if assigned(m) then mb := m.BoundsRect else mb := Screen.WorkAreaRect; // make sure top(window-bar) is visible if b.Top < mb.Top then OffsetRect(b, 0, mb.Top-b.Top); if b.Top + MinOverLap > mb.Bottom then OffsetRect(b, 0, mb.Top-b.Top-MinOverLap); // move left/right if b.Left + MinOverLap > mb.Right then OffsetRect(b, mb.Right-b.Left-MinOverLap, 0); if b.Right - MinOverLap < mb.Left then OffsetRect(b, mb.Left-b.Right+MinOverLap, 0); end; procedure THeapTrcViewForm.LoadState(cfg:TXMLConfig); var b : TRect; isTop : Boolean; st : TStringList; s : WideString; i : Integer; const InitFormStyle: array [Boolean] of TFormStyle = (fsNormal, fsStayOnTop); begin isTop:=True; b:=BoundsRect; st:=TStringList.Create; try istop:=cfg.GetValue('isStayOnTop',isTop); cfg.OpenKey('bounds'); b.Left:=cfg.GetValue('left', b.Left); b.Top:=cfg.GetValue('top', b.Top); b.Right:=cfg.GetValue('right', b.Right); b.Bottom:=cfg.GetValue('bottom', b.Bottom); cfg.CloseKey; if b.Right-b.Left<=0 then b.Right:=b.Left+40; if b.Bottom-b.Top<=0 then b.Bottom:=b.Top+40; for i:=0 to 7 do begin s:=cfg.GetValue(DOMString('path'+IntToStr(i)), ''); if s<>'' then st.Add(UTF8Encode(s)); end; except end; inAnyMonitor(b); FormStyle:=InitFormStyle[isTop]; BoundsRect:=b; chkStayOnTop.Checked := isTop; if st.Count>0 then begin edtTrcFileName.Items.AddStrings(st); edtTrcFileName.ItemIndex:=0; end; st.Free; end; procedure THeapTrcViewForm.AddFileToList(const FileName:AnsiString); var i : Integer; begin i:=edtTrcFileName.Items.IndexOf(FileName); if (i<0) then begin if edtTrcFileName.Items.Count=8 then edtTrcFileName.Items.Delete(7); end else edtTrcFileName.Items.Delete(i); edtTrcFileName.Items.Insert(0, FileName); end; procedure THeapTrcViewForm.LazarusJump(Sender: TObject; const SourceFile: string; Line, Column: Integer); var nm : string; begin if not FileExistsUTF8(SourceFile) then begin nm := LazarusIDE.FindSourceFile(SourceFile, '', [fsfUseIncludePaths] ); if nm = '' then nm := SourceFile; end else nm := SourceFile; LazarusIDE.DoOpenFileAndJumpToPos(nm, Point(Column, Line), -1, -1, -1, [ofOnlyIfExists, ofRegularFile]); end; destructor THeapTrcViewForm.Destroy; begin FreeAndNil(Finfo); inherited Destroy; end; procedure IDEMenuClicked(Sender: TObject); begin ShowHeapTrcViewForm(nil); end; procedure Register; var IDEShortCutX: TIDEShortCut; IDECommandCategory: TIDECommandCategory; IDECommand: TIDECommand; IDEButtonCommand: TIDEButtonCommand; begin RegisterIDEMenuCommand(itmViewMainWindows, 'mnuLeakView', rsLeakView, nil, @IDEMenuClicked); IDEShortCutX := IDEShortCut(VK_UNKNOWN, [], VK_UNKNOWN, []); IDECommandCategory := IDECommandList.FindCategoryByName(CommandCategoryViewName); if IDECommandCategory <> nil then begin IDECommand := RegisterIDECommand(IDECommandCategory, 'Leaks and Traces', rsLeakView, IDEShortCutX, nil, @IDEMenuClicked); if IDECommand <> nil then begin IDEButtonCommand := RegisterIDEButtonCommand(IDECommand); if IDEButtonCommand=nil then ; end; end; end; end.