Skip to content

perf(jq): transitively included modules compound in memory when a module's defs each call more than one def below #2955

Description

@newhoggy

Severity: Low

Summary

Found by code review on PR #2954 (#2865). #2865 makes a module's own includes
load transitively by wrapping each exported def's body in the defs it reaches.
Because that wrap copies the AST, a chain of modules compounds: each level's
bodies already carry the level below.

#2865 mitigates this with jq's own block_bind_referenced rule (wrap only what
the body transitively calls), which flattens the common shape completely. It does
not cure the general case: as soon as each def calls more than one def from
the level below, the referenced closure is itself exponential in chain depth.

Output is correct throughout — this is a memory/time scalability gap, not a
wrong answer, and not a regression (before #2865 these programs did not compile
at all).

Repro

Synthetic chains of modules, each def calling F defs from the level below,
resolving one def at the top. Apple M-series, release build, measured with
/usr/bin/time -l at PR #2954's head:

chain fan-out levels x defs succinctly peak RSS jq peak RSS
flat 1 6 x 40 10 MB 2.5 MB
wide 3 8 x 6 91 MB 2.6 MB
wide 2 14 x 4 361 MB 2.6 MB

Generator:

def gen(L, D, F, d):
    with open(f"{d}/m0.jq", "w") as f:
        for i in range(D):
            f.write(f"def f0_{i}: {i};\n")
    for lvl in range(1, L):
        with open(f"{d}/m{lvl}.jq", "w") as f:
            f.write(f"include \"m{lvl-1}\";\n")
            for i in range(D):
                calls = " + ".join(f"f{lvl-1}_{(i+k)%D}" for k in range(F))
                f.write(f"def f{lvl}_{i}: {calls};\n")

Then succinctly jq -L <dir> -nc 'include "m13"; f13_0'.

Root cause

ModuleLoader::load_and_bind_module / visible_defs_for
(src/bin/succinctly/jq_runner.rs): binding is done by cloning each
reachable def's Expr into the wrapping body. Real jq binds symbolically — its
block values are shared, so a def reachable by many paths exists once.
succinctly's module support is AST inlining, where "reachable by K paths" means
K copies.

Suggested fix direction

Three shapes, roughly in increasing order of cost:

  1. Share the bound bodies. Expr already has an Rc-shaded variant for
    exactly this purpose (the opaque already-substituted sub-expression jq: self-recursive user-defined functions can't work at any real depth under static AST substitution #1371
    introduced to stop recursive calls growing with depth). If a bound def body
    could be spliced by handle rather than by clone, the copies collapse to
    pointers. Needs a check that every later pass over the tree treats it
    correctly.
  2. Bind a module once into a single shared scope rather than per exported
    def — closer to jq's own block model, but it is the per-body wrapping that
    buys jq: a module's own include directive is silently ignored (transitive include not processed) #2865 its scoping rows (a dependency visible inside the module, not
    re-exported, outranking a same-name sibling), so this needs the scope
    boundary from jq: a module body sees names it should not — ~/.jq's defs, and sibling included modules' defs in declaration order #2951 to exist first.
  3. A depth/size guard that refuses with a clear error rather than
    exhausting memory, in the spirit of the other resource caps in
    docs/compliance/jq/limitations.md. A stopgap, not a fix.

Option 1 is the one worth costing first; #2951's sealed-scope work would
subsume option 2.

Why this is separate from #2865, #2951

#2865 is the functional gap (transitive includes not processed at all) and
ships the mitigation that handles the common shape; this is the residual
scalability limit of the mechanism it uses. #2951 is a scoping-correctness gap
whose fix would likely also change this mechanism, but it is not itself about
memory.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    FableSuitable for a Fable-class model to implementTriagedIssue has been read, planned, and its model-class label verifiedenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions