watchpack

repository·main·Indexed 18 days ago

https://github.com/webpack/watchpack

A high-level wrapper library for directory and file watching designed to minimize the number of active watchers. It provides an efficient mechanism for monitoring filesystem changes via the Watchpack class, supporting polling, symlink configuration, and ignored patterns. It emits change, remove, and aggregated events and includes a three-level architecture using WatcherManager and DirectoryWatchers to optimize overhead.

Tokens
3.9K
Snippets
8
Records
16
Agent score
63%

What's inside watchpack

  1. Understand the benchmark layout

    main

    The benchmark directory follows this structure:

    • run.mjs: The entry point that discovers cases, runs them, and prints the results table.
    • with-codspeed.mjs: A bridge between tinybench and @codspeed/core.
    • cases/<case-name>/index.bench.mjs: Individual benchmark scenario definitions.
    bench/
    ├── README.md
    ├── run.mjs             # entry: discovers cases, runs them, prints a table
    ├── with-codspeed.mjs   # tinybench <-> @codspeed/core bridge
    └── cases/
        └── <case-name>/
            └── index.bench.mjs
  2. How CodSpeed integration works

    main

    The benchmark runner (bench/run.mjs) wraps the Bench instance with withCodSpeed().

    • Local Execution: If CODSPEED_* environment variables are absent, the wrapper returns the bench untouched, and tinybench uses standard wall-clock timing.
    • CodSpeed Execution: When running under CodSpeedHQ/action with mode: "simulation", the bridge overrides bench.run to call instrumentation hooks once per task. This produces reproducible instruction-count measurements that are independent of the runner's system load.
  3. How watchpack's architecture works

    main

    watchpack uses a three-level architecture to optimize file watching and ensure that only a single watcher exists for each directory, keeping the total watcher count low.

    • WatcherManager & DirectoryWatchers: The high-level API requests DirectoryWatchers from a WatcherManager. This ensures that even if multiple parts of an application request to watch the same directory, only one DirectoryWatcher is actually created.
    • Watcher: A user-facing Watcher is obtained from a DirectoryWatcher. It provides a filtered view of the directory's activity.
    • Reference Counting: Both DirectoryWatcher and Watcher use reference counting to determine when it is safe to close them.
    • No Direct File Watching: Files are never watched directly; instead, they are tracked via their parent directories. This is a key mechanism for maintaining a low watcher count.
    • Symlinks: By default, symlinks are not followed; instead, the symlink itself is watched. This behavior can be changed via configuration.
    • Historical Watching: Watching can be started at a specific point in the past (using startTime), allowing you to start watching after you have already performed initial file reads.
  4. How to write a new benchmark case

    main

    To add a new performance scenario, follow these steps:

    1. Create a new directory under bench/cases/<name>/.
    2. Create an index.bench.mjs file in that directory.
    3. Export a default register(bench, ctx) function.
    4. Use bench.add(name, fn) to register tasks.

    Best Practices:

    • Fixture Setup: Pre-build expensive fixtures outside the benchmark callback so only the hot path is measured. The ctx.fixtureDir points to cases/<name>/fixture/.
    • Stability: Each bench.add body should loop over a fixed batch of inputs to ensure the measurement window sees enough work to be stable (minimum tens of microseconds).
    • Determinism: Avoid non-determinism; use fixed request lists and avoid Math.random().
    • Granularity: Focus one case on one scenario. For different shapes (e.g., warm vs. cold cache), create a new case directory instead of adding multiple bench.add calls to an existing one.

    The ctx argument contains:

    • caseName: The name of the case.
    • caseDir: The directory of the case.
    • fixtureDir: The directory for fixtures (cases/<name>/fixture/).
    // Example structure for bench/cases/<name>/index.bench.mjs
    export default function register(bench, ctx) {
      // Pre-build fixtures outside the callback
      const fixture = setupFixture(ctx.fixtureDir);
    
      bench.add("my-task", () => {
        // Loop over a fixed batch of inputs for stability
        for (let i = 0; i < 100; i++) {
          runTask(fixture);
        }
      });
    }
  5. Run watchpack benchmarks

    main

    Benchmarks for watchpack can be executed using npm run benchmark. You can run all available cases or filter them using the BENCH_FILTER environment variable or a positional argument to run only cases whose directory name contains a specific string.

    # Run every case
    npm run benchmark
    
    # Run only cases whose directory name contains "ignored"
    BENCH_FILTER=ignored npm run benchmark
    # or, equivalently
    npm run benchmark -- ignored
  6. How Watchpack aggregation works

    main

    Watchpack uses an aggregation mechanism to prevent overwhelming consumers with a flood of individual events during rapid file system changes (e.g., during a git checkout or a large build).

    1. When a change or remove event occurs, Watchpack emits the individual event immediately (if not paused).
    2. It then starts (or restarts) an internal timer based on aggregateTimeout.
    3. During this timeout, all subsequent changes and removals are collected into aggregatedChanges and aggregatedRemovals sets.
    4. Once the timer expires, the aggregated event is emitted with the full sets of changes and removals.

    This allows you to handle individual updates in real-time while also having a way to process bulk updates efficiently.

  7. Start watching files and directories with watch()

    main

    Call wp.watch() to start monitoring specific items. Calling this method again will override the previous files and directories being watched.

    Parameters:

    • files (Iterable<string>): Files or directories. For files, content and existence are tracked. For directories, only existence and timestamp changes are tracked.
    • directories (Iterable<string>): Only directories. Tracks directory content (and children) and existence. Assumed to exist; if not found, a remove event is emitted.
    • missing (Iterable<string>): Items expected to not exist. Only existence changes are tracked. No remove event is emitted when these are not found initially.
    • startTime (number, optional): A timestamp representing when watching should have started (e.g., Date.now() - 10000).
    wp.watch({
    	files: listOfFiles,
    	directories: listOfDirectories,
    	missing: listOfNotExistingItems,
    	startTime: Date.now() - 10000,
    });
  8. Retrieve time information for files and directories

    main

    Watchpack can provide metadata about when files and directories were last modified.

    • wp.getTimeInfoEntries(): Returns a Map containing time info for all known files and directories (including those not directly watched).

      • Key: Absolute path.
      • Value: { safeTime, timestamp }
        • safeTime: A point in time at which it is safe to say all changes happened before that.
        • timestamp: The mtime timestamp (for files).
    • wp.collectTimeInfoEntries(fileInfoEntries, directoryInfoEntries): Allows you to manually provide maps of file and directory entries to collect time info for them.

      • fileInfoEntries: Map<string, Entry>
      • directoryInfoEntries: Map<string, Entry>
  9. Initialize and configure Watchpack

    main

    To use watchpack, instantiate the Watchpack class with an options object.

    Options:

    • aggregateTimeout (number): Time in milliseconds to wait after a change before firing the aggregated event. If undefined, the aggregated event will not fire.
    • poll (boolean | number):
      • true: Use polling with the default interval.
      • number: Use polling with the specified interval (e.g., 10000 for 10s).
      • undefined (default): Use native watching methods.
      • Note: Enable polling when watching over a network path. The WATCHPACK_POLLING environment variable overrides this option.
    • followSymlinks (boolean):
      • true: Follows symlinks and watches both the symlink and the real files (higher performance hit).
      • false (default): Watches only the specified item (real file or symlink).
    • ignored (string | string[] | RegExp | function): Defines what should be ignored.
      • string: A glob pattern.
      • string[]: Multiple glob patterns.
      • RegExp: A regular expression.
      • (entry) => boolean: An arbitrary function returning truthy to ignore an entry.
      • Note: Path separators are normalized to /. All subdirectories of an ignored entry are also ignored.
    const Watchpack = require("watchpack");
    
    const wp = new Watchpack({
    	aggregateTimeout: 1000,
    	poll: true,
    	followSymlinks: true,
    	ignored: "**/.git",
    });
  10. Pause, resume, and close the watcher

    main

    Control the lifecycle of the watcher using pause() and close().

    • wp.pause(): Stops emitting events but keeps the underlying watchers open. The watcher continues to aggregate events in the background, which can be retrieved via getAggregated().
    • wp.close(): Stops emitting events and closes all underlying watchers completely.
    • wp.getAggregated(): Returns the current aggregated info and removes it from the watcher. This is useful when the watcher is paused.
      • Returns: { changes: Set<string>, removals: Set<string> }
    // Pause watching
    wp.pause();
    
    // Retrieve events that happened while paused
    const { changes, removals } = wp.getAggregated();
    
    // Close everything
    wp.close();
  11. Handle watchpack events: change, remove, and aggregated

    main

    Watchpack emits several events to notify you of filesystem changes.

    • change: Emitted when a file changes.

      • Callback: (filePath, mtime, explanation) => void
      • filePath: The changed file.
      • mtime: Last modified time.
      • explanation: Textual info on how the change was detected.
    • remove: Emitted when a file or directory is removed.

      • Callback: (filePath, explanation) => void
      • filePath: The removed item.
      • explanation: Textual info on how the change was detected.
    • aggregated: Emitted after a period of inactivity defined by aggregateTimeout.

      • Callback: (changes, removals) => void
      • changes: A Set of all changed files.
      • removals: A Set of all removed files.
      • Note: Watchpack gives up ownership of these Sets; do not modify them.
    wp.on("change", (filePath, mtime, explanation) => {
    	// handle change
    });
    
    wp.on("remove", (filePath, explanation) => {
    	// handle removal
    });
    
    wp.on("aggregated", (changes, removals) => {
    	// handle aggregated changes
    });
  12. Configure ignored patterns in Watchpack

    main

    The ignored option in Watchpack allows you to exclude specific files or directories from being watched. It accepts several formats:

    • String (Glob): A glob pattern (e.g., '**/node_modules/**').
    • RegExp: A standard JavaScript regular expression (e.g., /\.log$/).
    • Array of Strings: An array of glob patterns.
    • Function: A predicate function (item: string) => boolean that returns true if the item should be ignored.

    Note: Path separators are normalized to forward slashes (/) during the matching process to ensure consistency across platforms.