apex-dev-tools / apex-dev-tools/apex-log-parser
✨ feat: Answer "what variables were in scope at this frame" from the parser
- Langage dominant
- TypeScript
- Étoiles
- 2
- Forks
- 0
- Merge moyen
- 5 h 51 min
- PR mergées (30 j)
- 32
Description
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.
Guide de contribution
Ouvrir le guide de contribution
Évaluation
Cette issue n'a pas encore été évaluée.