xstate
xstate copied to clipboard
XState Inspector can be slow
Description XState inspector is slow with large context objects A trace is attached.
The active machines on the page have contexts that JSON.stringify() to the following lengths:
{
"main": 12482,
"actors": {
"0": 19786,
"1": 136995,
"2": 6564
}
}
Actors and the main machine send about 10 events in quick succession, and that makes the inspector lag.
Additional context Attached a trace: Profile-20210329T172918.zip
The workaround in https://github.com/davidkpiano/xstate/issues/2127 can be used to modify large context objects, and truncate the data that is not relevant to the inspector.
That workaround may work for Map and Set but other objects, when even moderately complex, cause the stringify to be very slow and cause the inspector to crash.
Here is an example:

That shows over 800ms to call stringify for the inspector which caused the UI to block on send(event, ...)
@engineersamuel
function stringifyToWarning<T extends Record<string, any>>(obj: T): T {
if (process.env.NODE_ENV === 'development' && typeof obj === 'object') {
;(obj as any)['toJSON'] = function () {
return 'Context too large for inspector'
}
}
return obj
}
const machine = createMachine({
context: stringifyToWarning({
// huge context
})
})
Here is a "big hammer" workaround for now :)
Hahaha, yes that would work! But the inspector is so amazing I wouldn't want to give it up, but technically I guess I wouldn't be giving it up, I could still see the state, just not the context here, so point taken. Likely part of the problem is the stringifying of more complex objects, likely I need to write a toJSON that ignores anything but primitive types and basic objects.
Yeah, that was my point in my previous comment - to do a toJSON() implementation that is specific to a given context, and simplifies it to only the most important parts you want to view inside the inspector.
So that may work on the initial context, but I think assign may be overriding it. What we really need is the built in ability to override the toJSON without hacking it like this. xstate should provide a function that can be overridden.
@engineersamuel Assign doesn't override this. I've tested it.
I don't think XState should provide a function here, given that it is perfectly solvable in userland. What XState might choose to do is, if it sees an object stringifies to more than 500 characters, it stops trying to stringify it.
@VanTanev I see, I dug deeper, and there is an issue still here. The toJSON is not called when invoking send. When invoking send the following code is invoked:
service.send = function inspectSend(event, payload) {
inspectService.send({
type: 'service.event',
event: stringify(toSCXMLEvent(toEventObject(event, payload))),
sessionId: service.sessionId
});
return originalSend(event, payload);
};
So you'll see the stringify is called there which does not respect the toJSON. I think the inspector likely respects it, but sending it to the inspector performs a full stringify:
utils.js:
function stringify(value) {
try {
return JSON.stringify(value);
}
catch (e) {
return safeStringify(value);
}
}

you can apply a similar fix to send event objects:
function shortStringify<T extends EventObject>(obj: T): T {
if (process.env.NODE_ENV === 'development' && typeof obj === 'object') {
;(obj as any)['toJSON'] = function () {
const keys = Object.keys(this)
const res = { ...this }
for (let key of keys) {
const value = this[key]
if (key === 'type') {
res[key] = value
} else {
res[key] = '...'
}
}
return res
}
}
return obj
}
send(shortStringify({ type: 'MY_EVENT', data /* HUGE */ }))
Awesome, that works, I made a slight change, will record here for others as well.
export function shortStringify<T>(obj: T): T {
if (process.env.NODE_ENV === 'development' && typeof obj === 'object') {
;(obj as any)['toJSON'] = function () {
const keys = Object.keys(this)
const res = { ...this }
for (let key of keys) {
const value = this[key]
if (key === 'type') {
res[key] = value
} else {
res[key] = '...'
}
}
return res
}
}
return obj
}
Then call send with:
send(MintEvent.SOME_EVENT, { value: shortStringify(largeObject) });
I'm fascinated by XState's visualization, and I'm currently developing all logic in the tree structure of the machine and the machine that is invoked. And I faced this problem.
The current problem is the initialization of my service, which calls up large objects through 11 asynchronous logic during the initialization process. To reuse asynchronous logic, enter parameters to retrieve other data using a wrapper machine with a promise machine at the bottom. I want the initialization to proceed automatically, so I use invoke.
So when I start the app, the root machine invokes the machine to delegate the work, and it repeats itself in a tree structure. As a result, the machine is created as a multiple of the operation and the inspector does not work well(The inspector cannot display the screen according to the amount of data in the context.). I am running XState in Node.js environment and the machine is working normally in Node.js, but the inspector is stopping.
Currently I do not send events directly, so in my use case, I think I need an option to override toJSON or omit the context within machine configure. If you have any ideas in this situation, can I get your advice?
The solution I used is as follows.
// ignore.ts
function ignore(obj: Record<string, any>, name?: string) {
return Object.assign(obj, { toJSON: () => `[Ignored object]${name ? ': ' + name : ''}`});
}
// assign.ts
import { assign as _assign, AssignAction, Assigner, EventObject, PropertyAssigner } from 'xstate';
import { ignore } from './ignore';
function resetIgnore(obj: any) {
delete obj['toJSON'];
}
export function assign<TContext, TEvent extends EventObject = EventObject>(params: Assigner<TContext, TEvent> | PropertyAssigner<TContext, TEvent>, options: Partial<{ ignore: boolean }> = {}) {
const result = _assign(params);
const transformIgnore = options.ignore ? ignore : resetIgnore;
if (typeof result.assignment === 'function') {
return {
...result,
assignment: (...params: any) => transformIgnore((result.assignment as any)(...params)),
};
} else {
const clonedAssignment = {} as any;
for (const key in result.assignment) {
clonedAssignment[key] = (...params: any) => transformIgnore((result.assignment as any)[key](...params));
}
return {
...result,
assignment: clonedAssignment,
};
}
}
// machine.ts
... // withConfig parameters
services: {
awaitPromise: (ctx) => ctx.createPromise().then(data => ignore(data)),
},
actions: {
assignValue: assign({
value: (ctx, event) => event.newValue,
}, { ignore: true }),
},
...
Here, ignore function is acting like a function you created. Assign function overwrites toJSON both when ignore is true and false, because data is always hidden when the toJSON of the received data is overwritten. I also worked on the Object.defineProperty, but I couldn't use it for primitive values, so I gave up and used to overwrite toJSON key.
My concerns are as follows. The toJSON field is included in the all data I want to ignore. This should be considered when using JSON.stringify in logic. Also, if it is an object, care should be taken when using a for in loop.
If I added toJSON so that I can't see all the data in the inspector, the inspector worked normally. I checked that the data was delivered as a reference instead of being duplicated, but it became slow when the inspector stringify the data. The 100 KB of data was enough to stop the inspector.... So if there's no way, I'd like it to go into the function of XState.