All files / lib/core/trace tracer.js

97.24% Statements 212/218
80.76% Branches 21/26
100% Functions 11/11
97.24% Lines 212/218

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 57x 57x 57x 57x 57x 505x 505x 505x 505x 505x 505x 58x 58x 58x 58x 58x 58x 505x 505x 505x 505x 505x 505x 6827x 6827x 505x 505x 505x 505x 505x 505x 505x 505x 505x 67x 1x 1x 66x 67x 505x 505x 505x 505x 505x 505x 505x 505x 505x 6718x 1x 1x 6717x 6717x 6717x 6717x 6717x 6717x 6718x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 36x     36x 36x 36x 36x 505x 505x 505x 505x 505x 505x 6753x 6752x 6752x 6753x 505x 505x 505x 505x 505x 505x 505x 505x 505x 2x 1x 1x 1x 1x 1x 1x 2x 505x 505x 505x 505x 505x 505x 505x 1x     1x 505x 505x 505x 505x 505x 505x 505x 505x 1x     1x 1x 1x 1x 1x 1x 1x 505x 505x 505x 505x 505x 505x 505x 505x 505x  
/**
 * @file tracer.js
 * @description The causal stack and hook surface the runtime records through.
 *
 * Every instrumented site in the runtime talks to this one module. It holds
 * two things: a pointer to the active recorder, and the *causal stack* — the
 * chain of nodes currently being executed, innermost last. A node emitted
 * while that stack is non-empty is attributed to whatever sits on top of it,
 * which is what turns a flat log into "this DOM patch happened because that
 * click ran that action which wrote that property".
 *
 * ## Cost when tracing is off
 *
 * Tracing is off by default and must stay free. Every hook site is written as
 * a guarded pair:
 *
 * ```js
 * const token = tracer.on ? tracer.enter(TYPE, data) : -1;
 * try { ... } finally { if (token >= 0) tracer.leave(token); }
 * ```
 *
 * With `on === false` that is one property read and one comparison; the
 * descriptor object is never allocated, because it only appears inside the
 * conditional. Nothing else in the runtime changes shape.
 *
 * ## Stack safety
 *
 * `enter` returns a restore token rather than expecting a balanced `leave`.
 * `leave` truncates the stack back to that depth, so an exception unwinding
 * past a `leave` cannot leave the tracer permanently mis-parented — the next
 * outer `leave` repairs it.
 * @module lib/core/trace/tracer
 */
 
import { TraceNodeType } from './schema.js';
 
/**
 * The active tracer.
 *
 * A singleton rather than a per-app instance: the reactive system, the
 * sandbox and the patcher are all module-scoped, so a per-app tracer would
 * need threading through call sites that have no app reference. Recording more
 * than one application at a time is not a use case V1 supports.
 */
class Tracer {
  /**
   * Constructs the inactive tracer installed at import time.
   */
  constructor() {
    /**
     * Hot-path flag. Read by every instrumented site in the runtime, so it is a
     * plain own property rather than a getter.
     * @type {boolean}
     */
    this.on = false;
 
    /**
     * The recorder receiving nodes, or null when tracing is off.
     * @type {object|null}
     */
    this.sink = null;
 
    /**
     * Ids of the nodes currently executing, innermost last.
     * @type {number[]}
     */
    this.stack = [];
  }
 
  /**
   * Begins recording into a sink.
   * @param {object} sink - The recorder to feed.
   * @returns {Tracer} This tracer.
   */
  attach(sink) {
    this.sink = sink;
    this.stack.length = 0;
    this.on = !!sink;
    return this;
  }
 
  /**
   * Stops recording and drops the sink.
   * @returns {object|null} The detached recorder.
   */
  detach() {
    const previous = this.sink;
    this.sink = null;
    this.on = false;
    this.stack.length = 0;
    return previous;
  }
 
  /**
   * The id of the node currently being executed, or null at the top level.
   * @returns {number|null} The causal parent for anything emitted right now.
   */
  current() {
    return this.stack.length > 0 ? this.stack[this.stack.length - 1] : null;
  }
 
  /**
   * Records a leaf node — something that happened, but that nothing else
   * happened *inside*.
   * @param {string} type - A {@link TraceNodeType}.
   * @param {object} data - Type-specific fields.
   * @returns {object|null} The stored node, or null when tracing is off.
   */
  record(type, data) {
    if (!this.sink) {
      return null;
    }
    return this.sink.push(type, data, this.current());
  }
 
  /**
   * Records a node and makes it the causal parent of everything emitted until
   * the matching {@link Tracer#leave}.
   * @param {string} type - A {@link TraceNodeType}.
   * @param {object} data - Type-specific fields.
   * @returns {number} A restore token to hand to `leave`, or -1 when tracing is off.
   */
  enter(type, data) {
    if (!this.sink) {
      return -1;
    }
    const token = this.stack.length;
    const node = this.sink.push(type, data, this.current());
    if (node) {
      this.stack.push(node.id);
    }
    return token;
  }
 
  /**
   * Re-enters an existing node's causal scope without recording a new node.
   *
   * The scheduler batches component updates into a microtask, so the DOM patch
   * caused by a click happens on an empty causal stack — long after the click
   * handler returned. Stamping the queued job with the node that queued it and
   * resuming that scope here is what keeps a patch attributed to the write
   * that caused it, rather than appearing as an unexplained root.
   * @param {number|null} id - The node id to continue from.
   * @returns {number} A restore token for `leave`, or -1 when tracing is off.
   */
  continueFrom(id) {
    if (!this.sink || id === null || id === undefined) {
      return -1;
    }
    const token = this.stack.length;
    this.stack.push(id);
    return token;
  }
 
  /**
   * Restores the causal stack to the depth a matching `enter` returned.
   * @param {number} token - The token from `enter`.
   */
  leave(token) {
    if (token >= 0 && this.stack.length > token) {
      this.stack.length = token;
    }
  }
 
  /**
   * Attaches a field to the node most recently entered.
   *
   * Used where the interesting value is only known once the node's body has
   * run — a computed's new value, an action's return, a resource's outcome.
   * @param {object} fields - Fields to merge into the open node.
   */
  annotate(fields) {
    if (!this.sink) {
      return;
    }
    const id = this.current();
    if (id !== null) {
      this.sink.annotate(id, fields);
    }
  }
 
  /**
   * Reports that something happened which replay cannot reproduce.
   * @param {string} reason - A non-determinism reason.
   * @param {string} [detail] - Extra context shown to the developer.
   */
  markNonDeterministic(reason, detail) {
    if (this.sink) {
      this.sink.markNonDeterministic(reason, detail);
    }
  }
 
  /**
   * Records an error that escaped application code.
   * @param {Error} error - The error.
   * @param {object} [context] - Where it came from.
   * @returns {object|null} The stored node.
   */
  recordError(error, context = {}) {
    if (!this.sink) {
      return null;
    }
    return this.record(TraceNodeType.ERROR, {
      name: (error && error.name) || 'Error',
      message: String((error && error.message) || error),
      code: error && error.code,
      ...context,
    });
  }
}
 
/**
 * The process-wide tracer. Inactive until a recorder attaches to it.
 * @type {Tracer}
 */
export const tracer = new Tracer();
 
export { Tracer };