fix(rbxl): watchdog 100k→20k + откат pcall(yield) + batch 5+20ms

Прошлый коммит pcall(coroutine.yield) дал бесконечный цикл:
yield внутри C-call падал → pcall ловил → hook возвращался → счётчик
не сбросился → срабатывал опять моментально → вис.

Новая стратегия:
1. Голый coroutine.yield в watchdog: если внутри C-call упадёт с
   ошибкой — pcall(fn,...) внутри coroutine её поймает, скрипт
   завершится. Лучше чем вис.
2. Frequency 100k→20k инструкций — yield чаще, меньше времени на
   tight-loop перед уступкой управления UI.
3. Batch kickoff 20→5 скриптов с delay 20мс (было 0). 55 скриптов
   ROBLOX Battle = ~200мс распределено, UI отзывается.

Page-hang при init должен исчезнуть. Скрипты с tight-loop типа
WaitForChild через ChildAdded:wait() упадут с ошибкой про yield,
но не повесят страницу.
This commit is contained in:
min 2026-06-08 20:51:24 +03:00
parent 734521df72
commit 38d135586b
2 changed files with 9 additions and 13 deletions

View File

@ -115,8 +115,8 @@ export class LuaSharedSandbox {
// Запускаем main-loop сразу — он начнёт tick'ать как только будут coroutines. // Запускаем main-loop сразу — он начнёт tick'ать как только будут coroutines.
this._lastTickAt = performance.now(); this._lastTickAt = performance.now();
this._startMainLoop(); this._startMainLoop();
// Init батчами по 20 с yield между ними, чтобы UI не подвисал на 700+ скриптах. // Init батчами по 5 с задержкой 20мс между ними, чтобы UI отзывался.
const BATCH_SIZE = 20; const BATCH_SIZE = 5;
let idx = 0; let idx = 0;
const initBatch = () => { const initBatch = () => {
if (this._isStopped) return; if (this._isStopped) return;
@ -130,7 +130,7 @@ export class LuaSharedSandbox {
} }
idx = end; idx = end;
if (idx < pending.length) { if (idx < pending.length) {
setTimeout(initBatch, 0); setTimeout(initBatch, 20);
} else { } else {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`[LuaSharedSandbox] all ${pending.length} scripts kicked off`); console.log(`[LuaSharedSandbox] all ${pending.length} scripts kicked off`);
@ -199,13 +199,11 @@ export class LuaSharedSandbox {
}) })
local co = coroutine.create(function() local co = coroutine.create(function()
-- WATCHDOG: каждые 100000 инструкций yield 1 кадр. -- WATCHDOG: каждые 100000 инструкций yield 1 кадр.
-- Защищает от tight-loop. yield обёрнут в pcall так как -- НЕ оборачиваем в pcall внутри C-call boundary yield
-- внутри C-call boundary yield бросает ошибку но в этом -- упадёт ошибкой, что прервёт скрипт. Это лучше чем виснуть.
-- случае tight-loop наш hook просто будет вызываться позже
-- (когда Lua вернётся из C-call) и yield сработает.
debug.sethook(function() debug.sethook(function()
pcall(coroutine.yield, 0.016) coroutine.yield(0.016)
end, "", 100000) end, "", 20000)
-- pcall защищает от runtime-ошибок которые иначе крашат -- pcall защищает от runtime-ошибок которые иначе крашат
-- coroutine и могут повредить WASM-стейт. Возвраты -- coroutine и могут повредить WASM-стейт. Возвраты
-- handler'а намеренно поглощаются. -- handler'а намеренно поглощаются.

View File

@ -1642,11 +1642,9 @@ export function registerRobloxShim(lua, opts) {
-- (ok, ret1, ret2, ...) мы их не используем. -- (ok, ret1, ret2, ...) мы их не используем.
local co = coroutine.create(function() local co = coroutine.create(function()
-- Тот же watchdog что и в _startSingleScript. -- Тот же watchdog что и в _startSingleScript.
-- yield обёрнут в pcall: внутри C-call boundary yield бросает
-- ошибку, но hook будет вызван позже когда Lua вернётся.
debug.sethook(function() debug.sethook(function()
pcall(coroutine.yield, 0.016) coroutine.yield(0.016)
end, "", 100000) end, "", 20000)
pcall(fn, a1, a2, a3, a4) pcall(fn, a1, a2, a3, a4)
end) end)
__rbxl_register_coroutine(handlerId, co) __rbxl_register_coroutine(handlerId, co)