apex-dev-tools / apex-dev-tools/apex-log-parser
✨ feat: Answer "what variables were in scope at this frame" from the parser
- Vorherrschende Sprache
- TypeScript
- Sterne
- 2
- Forks
- 0
- Ø Merge
- 5 Std. 51 Min.
- Gemergte PRs (30 T.)
- 32
Beschreibung
Related to #71, but larger: the field names are the easy half.
### Problem
#### The ask
A consumer that wants the variables a frame could see has to rebuild the whole thing from raw lines. We have now done that in [certinia/debug-log-analyzer](https://github.com/certinia/debug-log-analyzer) (PRs #1006 and #1009 there), and most of the ~1,000 lines is not about the UI at all: it is about what the log means.
#### What it takes, and why none of it is obvious
Each of these cost a measurement pass over real FINEST logs to establish.
- **Locals** are the frame's own writes, plus the names it declared and never assigned, which are in scope at their default. The log emits no `VARIABLE_SCOPE_END`, so two locals of one name in different blocks cannot be told apart outside their own frame.
- **The scope frame is not the selected frame.** A SOQL statement, a `STATEMENT_EXECUTE` or a system call owns no locals of its own. The answer has to come from the nearest frame above that records a variable, or a query built two lines earlier reads as an empty scope.
- **`this` and its fields.** A `this.field` line's trailing address identifies the *owning object*, not the value written. Grouping by that address finds fields a constructor set before it returned, which a walk of the frames on the stack cannot see. Field writes whose line reports no address also exist, so both sources have to merge with the latest write winning.
- **Statics** live for the whole transaction and are visible wherever their class is. The log qualifies them with the class.
- **Every value is as the frame stood**: the last write at or before a cut on the `eventIndex` axis. Timestamps repeat, so they order nothing.
- **Addresses.** Where a value will not serialise, the log writes a bare address, and may write that object's contents against the same address elsewhere: before the frame, after it, or never.
- **Address reuse.** An address is reused once its object is collected, so an object's history must be bounded by the run of the class living there. A construction always starts a new run, even of the same class, and a superclass constructor running on the same object must not overwrite the concrete class.
- **Caps**, per static name and per field, so one name written in a loop cannot grow the index without bound or evict the rest of its object.
### Proposed solution
#### Shape that worked for us
- One index, built lazily per log in a single walk that yields between slices: tens of ms on a 9MB log, under 100ms on a 19MB one, with no measurable retained heap.
- `variablesAt(eventIndex)` → locals, `this` and its fields, statics grouped by class.
- `addressState(address, cut)` → what the log holds for an address as of a cut, or where it describes that object later.
- `classAt(address, cut)` and `fieldsAt(address, cut)` for stepping down into an object.
An API like this would mean any consumer gets the semantics right by default, rather than re-deriving them. Happy to port ours as a PR if the shape suits.
### Alternatives considered
_None recorded._ Rebuilding this per consumer is the status quo, and is what the issue argues against.
Beitragsleitfaden
Bewertung
Dieses Issue wurde noch nicht bewertet.