Skip to content

Transformer walks the AST once per config instead of once per file, worth optimizing? #83

Description

@amychisholm03

Problem Statement

I was working on one of our instrumentation refactors, and I ended up having dozens of configs (via @apm-js-collab/tracing-hooks) per one source code file. Ultimately, that wasn't the best way of doing things, but it did reveal that Transformer.transform walks the AST once per config, resulting in N traversals of the same AST for a file.

I am curious if this is an observation other APM vendors have made and that if the following optimization would be worth it. Essentially, the idea is that we walk the AST once per file rather than once per config.

Potential Solution

Walk the AST once for all configs. Collect every config's selector up front, traverse the union of them a single time, and at each matched node dispatch to each config whose own selector matches:

const matchers = [] // { state, query, selector } per config

for (const config of this.#configs) {
  // ...build query/state as before...
  matchers.push({ state, query, selector: esquery.parse(query) })
}

if (matchers.length > 0) {
  const combined = esquery.parse(matchers.map(({ query }) => query).join(', '))
  esquery.traverse(ast, combined, (node, parent, ancestry) => {
    for (const { state, selector } of matchers) {
      if (esquery.matches(node, selector, ancestry)) {
        injectionCount++
        this.#visit(state, node, parent, ancestry)
      }
    }
  })
}

This takes traversals from N down to 1, regardless of how many configs target the file. But I think this is a constant-factor win — total matching work stays O(configs × AST nodes), since at each node we still check it against every config's selector. What we actually save is running esquery's tree-walk machinery once instead of once per config. So I'm curious whether that overhead is meaningful enough in practice to be worth it, or if I'm just working around a config setup I shouldn't have had in the first place.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions