facebook / react

The library for web and native user interfaces.

Home Page:https://react.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Bug: Maximum call stack size exceeded (React Devtools)

wavesoft opened this issue · comments

I encountered the same issue as #20640 but using react-devtools as a stand-alone app instead of from the the browser.

The bottom line is that the profiler becomes unresponsive after any interaction with a heavily loaded react page.

React version:

  • React: 17.0.2
  • ReactDOM: 17.0.2
  • React Devtools: 4.18.0

Steps To Reproduce

  1. Attach a react-devtools stand-alone (yarn run react-devtools) to a session
  2. Start profiling
  3. Do something on your app that would cause a huge number of updates
  4. The profiler stops functioning

Diagnostics

I actually went through and found out the origin of the issue, however I am not sure what would be the best approach to fix it.

It seems the the origin is a (very) big incoming message:

image

That eventually causes a maximum call stack exceeded error:

image

Coming from util.js:128:

export function utfDecodeString(array: Array<number>): string {
  return String.fromCodePoint(...array);
}

And interestingly enough, doing spread operator with an array with 235124 elements actually causes a Maximum call stack size exceeded error 😄

I tried replacing the code above with the following replacement and it seems to work:

export function utfDecodeString(array: Array<number>): string {
  return array.map(c => String.fromCodePoint(c)).join("")
}

@wavesoft Can you share a repro case with me please? I'd love to verify the fix (and if possible maybe even add an automated regression test– although this might not be possible, given the underlying problem seems to be with the generated code not the source code).

And interestingly enough, doing spread operator with an array with 235124 elements actually causes a Maximum call stack size exceeded error 😄

That is interesting! I don't think this is true for the native spread operator. For example, this runs okay for me in Chrome:

[...new Array(1_000_000).fill(true)]

I'm pretty sure this has to do with the code that's generated in the build (for backwards compat):

function utfDecodeString(array) {
  return String.fromCodePoint.apply(String, _toConsumableArray(array));
}

// Babel generated polyfill code below...

function _toConsumableArray(arr) {
  return (
    _arrayWithoutHoles(arr) ||
    _iterableToArray(arr) ||
    utils_unsupportedIterableToArray(arr) ||
    _nonIterableSpread()
  );
}

function _nonIterableSpread() {
  throw new TypeError(
    "Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
  );
}

function _iterableToArray(iter) {
  if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter))
    return Array.from(iter);
}

function utils_unsupportedIterableToArray(o, minLen) {
  if (!o) return;
  if (typeof o === "string") return utils_arrayLikeToArray(o, minLen);
  var n = Object.prototype.toString.call(o).slice(8, -1);
  if (n === "Object" && o.constructor) n = o.constructor.name;
  if (n === "Map" || n === "Set") return Array.from(o);
  if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))
    return utils_arrayLikeToArray(o, minLen);
}

function _arrayWithoutHoles(arr) {
  if (Array.isArray(arr)) return utils_arrayLikeToArray(arr);
}

function utils_arrayLikeToArray(arr, len) {
  if (len == null || len > arr.length) len = arr.length;
  for (var i = 0, arr2 = new Array(len); i < len; i++) {
    arr2[i] = arr[i];
  }
  return arr2;
}

This throws as you describe:

test(new utfDecodeString(1_000_000))

That is interesting! I don't think this is true for the native spread operator. For example, this runs okay for me in Chrome:

Actually I think this depends on the function. Indeed your array example works as expected, but try this:

String.fromCharCode(...Array(1_000_000))

For me it fails natively on chrome 😉

EDIT: I just realised you are doing a spread operator on an array. This is trivially translated into a concat call and the stack is not used. However if you try to use the spread operation in a function call, all the function arguments must be first placed on stack before the function is called (per standard JS function call procedure). This means that if the array is big enough, you will exhaust your stack.

@wavesoft Can you share a repro case with me please?

Unfortunately my project structure is pretty complex to identify just one possible code path that is causing that issue. My best guess is that it's happening when I am replacing (un-mounting and re-mounting) a couple of thousand of child elements at once. I can try to isolate a reproducible example it but it will take some time...

If it suits you though, I can easily give you a JSON dump of the contents of the said "operations" event that caused the exception.

Nice follow up information! Thanks!

Unfortunately my project structure is pretty complex to identify just one possible code path that is causing that issue.

Fair enough. I think we can proceed without a repro in this case.