views:

273

answers:

3

I wrote Delphi debug visualizer for TDataSet to display values of current row, source + screenshot: http://delphi.netcode.cz/text/tdataset-debug-visualizer.aspx . Working good, but very slow. I did some optimalization (how to get fieldnames) but still for only 20 fields takes 10 seconds to show - very bad.

Main problem seems to be slow IOTAThread90.Evaluate used by main code shown below, this procedure cost most of time, line with ** about 80% time. FExpression is name of TDataset in code.

procedure TDataSetViewerFrame.mFillData;
var
 iCount: Integer;
 I: Integer;
 //  sw: TStopwatch;
 s: string;
 begin
 //  sw := TStopwatch.StartNew;
   iCount := StrToIntDef(Evaluate(FExpression+'.Fields.Count'), 0);
   for I := 0 to iCount - 1 do
   begin
     s:= s + Format('%s.Fields[%d].FieldName+'',''+', [FExpression, I]);
  //  FFields.Add(Evaluate(Format('%s.Fields[%d].FieldName', [FExpression, I])));
     FValues.Add(Evaluate(Format('%s.Fields[%d].Value', [FExpression, I]))); //**
   end;
 if s<> '' then
   Delete(s, length(s)-4, 5);
 s := Evaluate(s);
 s:= Copy(s, 2, Length(s) -2);
 FFields.CommaText := s;
{  sw.Stop;
 s := sw.Elapsed;
 Application.MessageBox(Pchar(s), '');}
end;

Now I have no idea how to improve performance.

+5  A: 

That Evaluate needs to do a surprising amount of work. The compiler needs to compile it, resolving symbols to memory addresses, while evaluating properties may cause functions to be called, which needs the debugger to copy the arguments across into the debugee, set up a stack frame, invoke the function to be called, collect the results - and this involves pausing and resuming the debugee.

I can only suggest trying to pack more work into the Evaluate call. I'm not 100% sure how the interaction between the debugger and the evaluator (which is part of the compiler) works for these visualizers, but batching up as much work as possible may help. Try building up a more complicated expression before calling Evaluate after the loop. You may need to use some escaping or delimiting convention to unpack the results. For example, imagine what an expression that built the list of field values and returned them as a comma separated string would look like - but you would need to escape commas in the values themselves.

Barry Kelly
Ok, as quick test I built list of field values (without escape) and total time is now about 7s (without escape), but replaced "s.Fields[%d].Value" to "%s.Fields[%d].AsString"I know that in evaluator can be executed single procedure, but can I declare variable or execute sequence statement? something like var l:TStringList; ds.GetFieldsNames(l); Result:= l.CommaText; l.free;
netcodecz
A: 

I have not had a chance to play with the debug visualizers yet, so I do not know if this work, but have you tried using Evaluate() to convert FExpression into its actual memory address? If you can do that, then type-cast that memory address to a TDataSet pointer and use its properties normally without going through additional Evaluate() calls. For example:

procedure TDataSetViewerFrame.mFillData; 
var 
  DS: TDataSet;
  I: Integer; 
  //  sw: TStopwatch; 
begin 
  //  sw := TStopwatch.StartNew; 
  DS := TDataSet(StrToInt(Evaluate(FExpression)); // this line may need tweaking
  for I := 0 to DS.Fields.Count - 1 do 
  begin 
    with DS.Fields[I] do begin
      FFields.Add(FieldName);
      FValues.Add(VarToStr(Value));
    end;
  end; 
  {
  sw.Stop; 
  s := sw.Elapsed; 
  Application.MessageBox(Pchar(s), '');
  } 
end; 
Remy Lebeau - TeamB
good idea, but there is a problem: Evaluate('@'+FExpression) return 0, Evaluate(Format('Addr(%s)', [FExpression])) return different address than Addr(dataset) or @dataset in watch list in the IDE -> AV
netcodecz
For example from visualizer return $23F288 from IDE in same moment return @ds1 = $AC46AC. Code: iAddr := StrToInt(Evaluate(Format('Addr(%s)', [FExpression]))); Application.MessageBox(Pchar(IntToStr(i)), '');// debug DS := TDataSet(iAddr);
netcodecz
The address of the actual TDataSet object being pointed at is different than the address of taking the address of the TDataSet pointer itself. IOW: ds1 <> @ds1. When I get a chance, I will play with the visualizers and see what can be done with them.
Remy Lebeau - TeamB
Good point, I know - my mistake, iAddr := StrToInt(Evaluate(Format('Integer(%s)', [FExpression]))); returning correct addr but using this address resulting in AV.
netcodecz
Playing with it, I see the OTA evaluator is able to return memory addresses and integer values as well as strings. If you pass the visualizer Expression by itself to IOTAThread90.Evaluate(), it can return the memory address of the TDataSet object being evaluated. However, it is the memory address *as known by the debugged executable*, and thus is not valid within the debugger's address space. So either stick with Evaluate() (which is more powerful then the examples use) or else use IOTAProcess60.ReadProcessMemory() to manually read data from the debugged executable's address space.
Remy Lebeau - TeamB
A: 

Because Delphi is a different process than your debugged exe, you cannot direct use the memory pointers of your exe, so you need to use ".Evaluate" for everything.

You can use 2 different approaches:

  1. Add special debug dump function into executable, which does all value retrieving in one call
  2. Inject special dll into exe with does the same as 1 (more hacking etc)

I got option 1 working, 2 should also be possible but a little bit more complicated and "ugly" because of hacking tactics... With code below (just add to dpr) you can use:

Result := 'Dump=' + Evaluate('TObjectDumper.SpecialDump(' + FExpression + ')');

Demo code of option 1, change it for your TDataset (maybe make CSV string of all values?):

unit Unit1;

interface

type
  TObjectDumper = class
  public
    class function SpecialDump(aObj: TObject): string;
  end;

implementation

class function TObjectDumper.SpecialDump(aObj: TObject): string;
begin
  Result := ''; 
  if aObj <> nil then 
    Result := 'Special dump: ' + aObj.Classname;
end;

initialization
  //dummy call, just to ensure it is linked c.q. used by compiler
  TObjectDumper.SpecialDump(nil);

end.

Edit: in case someone is interested: I got option 2 working too (bpl injection)

André