Skip to content

Dev server progress goes backwards mid-compilation (non-monotonic percentage forwarded to reporters) #1459

Description

@kacper-mikolajczak

Describe the bug

During development, the build progress percentage Re.Pack reports for a platform decreases in the middle of a single compilation. Re.Pack forwards the bundler's percentage straight through to both consumers, so the value shown to the developer visibly runs backwards.

What I expect: within one compilation, the reported progress never goes down.

What actually happens (single platform, single compilation, development build):

63% -> 63% -> 27% -> 64% -> 64% -> ... -> 100%

Two things make this worth reporting separately from any app:

  1. It is not caused by Re.Pack's own logic. The reproduction below drives rspack's ProgressPlugin directly with one configuration - no React Native app, no second platform, no watch mode - and the percentage handed to the handler still decreases. Re.Pack then forwards that value unmodified on both code paths:
    • packages/repack/src/commands/rspack/Compiler.ts:43 - const percentage = Math.floor(value * 100); forwarded to every progressSenders[platform] and into the progress: { platform, value } message
    • packages/repack/src/commands/webpack/Compiler.ts:163 - const percentage = Math.floor(value.percentage * 100);, same pattern (I verified the rspack path empirically; for the webpack path I am reading the code, not reporting a measurement)
  2. Nothing upstream clamps it. Math.floor(value * 100) is the whole transformation, and I found no monotonicity guard in main (@ 8fba597) or in the published 5.3.0 / 5.4.0-canary-20260913172854 builds.

The shape of the drop points at a modules-ratio whose total grows as modules are discovered. From a real run, with the plugin's own message next to each percentage, the total jumps while the percentage falls:

62% | build modules (183)
63% | build modules (219)
63% | build modules (255)
27% | build modules (289)     <-- reported percentage falls
64% | build modules (919)     <-- module total jumped ~3.2x
64% | build modules (955)

Impact for us: the React Native dev-loading view and the terminal both show a counter that goes backwards mid-build, which reads like a stuck or restarting build. Anything downstream that assumes monotonic input (progress bars, animations, code waiting for a percentage threshold) inherits the problem.

Proposal: clamp the forwarded value per platform - keep the maximum seen so far within a compilation, and reset per platform when that platform's compilation restarts (a reset that clears every platform would let another platform's restart un-clamp this one on a multi-platform dev server). I am happy to open a PR if you agree the guard belongs in Re.Pack rather than in each app.

System Info

The reproduction is standalone and does not involve a React Native app, so a react-native info dump would describe a different project than the one that reproduces the bug. This is the environment the output below was produced in:

macOS 26.5.2 (arm64)
node 26.7.0
npm 11.19.0
@rspack/core 2.2.4
@callstack/repack 5.3.0

Happy to post a full npx react-native info from the app where this is user-visible if that is more useful than the standalone case.

Re.Pack Version

5.3.0 in the app where the symptom is visible to developers.

Checked and also affected - the forwarding code is unchanged: main at 8fba59747fe1 and 5.4.0-canary-20260913172854 (no clamp anywhere; Math.floor(value * 100) is the only transformation). Note the reproduction below needs only @rspack/core, so the underlying report is independent of the Re.Pack version - Re.Pack's part is forwarding a value that can regress.

Reproduction

Inline below rather than a repository link: the whole reproduction is one dependency-free script (@rspack/core only), and a link would add a step without adding information. Say the word and I will publish it as a gist or a tiny repository.

mkdir repro && cd repro && npm init -y && npm i @rspack/core && node repro.mjs

Steps to reproduce

  1. mkdir repro && cd repro && npm init -y && npm i @rspack/core
  2. Save the script below as repro.mjs
  3. node repro.mjs

It builds a synthetic module graph: a long serial chain (so progress climbs smoothly while modules are discovered one step at a time), with one wide subtree hanging off the middle of the chain so the module total jumps late in the build. A small loader adds a delay per module so the plugin's throttled handler samples the module-build phase repeatedly. No React Native, no Re.Pack, no watch mode, one configuration.

/**
 * Minimal reproduction: a single rspack (React Native dev server uses one child
 * compiler per platform) compilation reports a NON-MONOTONIC progress percentage.
 *
 * usage: node repro.mjs          (needs @rspack/core resolvable)
 */
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { rspack } from '@rspack/core';

const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'progress-monotonic-'));
const src = path.join(dir, 'src');
fs.mkdirSync(src);
const write = (name, code) => fs.writeFileSync(path.join(src, name), code);

// A serialized dependency chain: modules are discovered step by step, so the
// completed/total ratio climbs steadily and the percentage rises to ~64%.
const DEPTH = 60;
for (let c = 0; c < DEPTH; c++) {
  let code = '';
  for (let k = 0; k < 8; k++) {
    write(`l${c}_${k}.js`, `export const v = ${k};\n`);
    code += `import { v } from './l${c}_${k}.js';\n`;
  }
  // Halfway through the chain, one module pulls in a wide subtree. The module
  // count jumps from ~500 to ~1100 while only a handful of modules complete.
  if (c === DEPTH / 2) code += `import { big } from './big.js';\n`;
  if (c < DEPTH - 1) code += `import { next } from './c${c + 1}.js';\n`;
  code += 'export const next = 1;\n';
  write(`c${c}.js`, code);
}
write(
  'big.js',
  Array.from({ length: 600 }, (_, k) => {
    write(`d${k}.js`, `export const d = ${k};\n`);
    return `import { d } from './d${k}.js';`;
  }).join('\n') + '\nexport const big = 1;\n'
);
write('entry.js', `import { next } from './c0.js';\nexport default next;\n`);

const samples = [];
const compiler = rspack({
  name: 'ios',
  mode: 'development',
  entry: path.join(src, 'entry.js'),
  output: { path: path.join(dir, 'dist'), filename: 'main.js' },
  // Slows each module build so ProgressPlugin's 100ms throttle samples the
  // module-build phase several times instead of twice.
  module: {
    rules: [
      {
        test: /[\\/]src[\\/]/,
        use: [{ loader: path.join(dir, 'delay-loader.mjs') }],
      },
    ],
  },
  plugins: [
    new rspack.ProgressPlugin((percentage, msg) => {
      samples.push({ pct: Math.floor(percentage * 100), msg });
    }),
  ],
});
fs.writeFileSync(
  path.join(dir, 'delay-loader.mjs'),
  'export default function loader(source){const cb=this.async();setTimeout(()=>cb(null,source),25);return undefined;}\n'
);

compiler.run((error, stats) => {
  if (error) throw error;
  if (stats.hasErrors())
    console.error(stats.toJson({ all: false, errors: true }).errors.slice(0, 2));
  let prev = -1;
  const decreases = [];
  for (const sample of samples) {
    if (sample.pct < prev) {
      const i = samples.indexOf(sample);
      decreases.push(`${samples[i - 1].pct}% -> ${sample.pct}% (${sample.msg})`);
    }
    prev = sample.pct;
  }
  console.log('module-build phase:', samples.map((s) => s.pct).join(' -> '));
  console.log(
    'NON-MONOTONIC DECREASES:',
    decreases.length ? decreases.join(', ') : 'none'
  );
  compiler.close(() => {});
});

Observed today with @rspack/core 2.2.4, three consecutive runs of the unmodified script:

# 2.2.4, run 1
module-build phase: 8 -> 9 -> 10 -> 25 -> 55 -> 59 -> 61 -> 62 -> 62 -> 63 -> 63 -> 38 -> 64 -> 64 -> 64 -> 64 -> 64 -> 64 -> 64 -> 69 -> 70 -> 70 -> 71 -> 74 -> 75 -> 75 -> 77 -> 78 -> 80 -> 83 -> 87 -> 93 -> 93 -> 98 -> 100
NON-MONOTONIC DECREASES: 63% -> 38% (build modules (460))

# 2.2.4, run 2
module-build phase: 8 -> 9 -> 10 -> 25 -> 55 -> 59 -> 61 -> 62 -> 62 -> 63 -> 63 -> 27 -> 64 -> 64 -> 64 -> 64 -> 64 -> 64 -> 64 -> 69 -> 70 -> 70 -> 71 -> 74 -> 75 -> 75 -> 77 -> 78 -> 80 -> 83 -> 87 -> 93 -> 93 -> 98 -> 100
NON-MONOTONIC DECREASES: 63% -> 27% (build modules (283))

# 2.2.4, run 3
NON-MONOTONIC DECREASES: 63% -> 27% (build modules (292))

The same script reproduces on @rspack/core 2.1.3 (NON-MONOTONIC DECREASES: 63% -> 27% (build modules (283))), so this is not new in the latest release.

The dip magnitude varies per run because it depends on where the module total jumps relative to the handler's throttle window. Applying only a per-compilation maximum to the same script, changing nothing else, makes every sample monotonic:

NON-MONOTONIC DECREASES: none

In the app, the same value is what reaches the terminal reporter and the React Native dev-loading view, so this is what developers see. We currently carry a local patch that clamps the forwarded percentage per platform as a workaround, and would much rather delete it once the guard lives here.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions