What would optional memoization in grunt.file.expand mean for long-running build pipelines?
0 reputation · 01 Sept 2020, 12:04 UTC
0 reputation · 01 Sept 2020, 12:04 UTC
Grunt's grunt.file.expand and grunt.file.expandMapping currently perform synchronous filesystem reads on every invocation with no internal caching. This design avoids stale results but can impose repeated I/O overhead when the same patterns are evaluated multiple times during a single build.
The Grunt codebase has not introduced optional memoization for these APIs, though the possibility has been discussed as a performance optimization for large file sets. If a future version added an opt-in cache layer, it would need to coexist with existing plugin-level watch caches (such as those in grunt-contrib-watch) that already manage their own invalidation strategies.
Understand the design constraints and invalidation requirements that would govern any optional memoization feature, so that build authors can anticipate whether such a change would reduce I/O without re-introducing the stale-file-list problems that currently only appear at the plugin layer.
What invalidation triggers would a core memoization layer need to respect? How would it interact with watch plugins that already maintain separate caches? Would the feature be scoped per-task, per-target, or per-process?
35775 reputation · 01 Sept 2020, 19:05 UTC
An opt-in memoization layer for grunt.file.expand and grunt.file.expandMapping would reduce repeated filesystem scans when the same glob patterns are evaluated multiple times within a single build run. For long-running or watch-driven pipelines, the feature would only be safe if its cache lifetime is strictly bounded by task execution boundaries and invalidated on directory membership changes (add, remove, rename)—not merely on file content modifications. It must not outlive the watch event that triggers a rebuild, otherwise it would serve stale file lists to the very tasks the watcher just re-invoked.
A core memoization cache would need to respect these triggers:
add/unlink/change events from the watcher and evict affected pattern entries immediately.The watch plugin already maintains its own file set and triggers task reruns on changes. A core cache that persists across watch events creates a race: the watcher detects a change, re-runs tasks, but the memoized expand returns the pre-change file list. To avoid this, either:
grunt.file.invalidate(pattern) before re-running tasks, orWithout one of these, the core cache re-introduces the stale-file-list problem that currently only appears at the plugin layer.
| Scope | I/O Savings | Staleness Risk | Safe Default? |
|---|---|---|---|
| Per-process | Maximum (all tasks share cache) | High — survives watch cycles, requires robust invalidation | No |
| Per-target | Moderate (shared within multi-target task) | Medium — survives target switches within a run | Opt-in only |
| Per-task-run | Limited (only repeated calls within one task) | Low — cleared before next task/watch cycle | Yes |
expandMapping adds a second dimension: stale src caches produce stale or orphaned destination mappings, breaking incremental-build correctness in plugins that compare mtimes.
grunt.file.expand to count calls and measure time per pattern. Confirm repeated-pattern I/O is the actual bottleneck.lib/grunt/file.js in your installed Grunt version to confirm no caching exists and see how expand delegates to glob.add/unlink events and diff returned file lists against uncached expand results.What is the typical number of grunt.file.expand calls per pattern per build run in your pipeline? If it's 1–2, memoization yields negligible benefit regardless of design.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.