dikdimon commited on
Commit
61220ba
·
verified ·
1 Parent(s): 67a8615

Upload 4 files

Browse files
!0000-a1111-fix/CHANGES.md ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Что изменилось и почему
2
+
3
+ Всё ниже проверено кодом, а не на словах: тестовые скрипты и их вывод — в
4
+ конце файла. Каждый пункт можно перепроверить самостоятельно.
5
+
6
+ ## Новое: `extensions/!0000-a1111-fix/scripts/backend_watchdog.py`
7
+
8
+ Главный кусок. Отвечает на исходный вопрос ("зависает + пропадает API"),
9
+ которого `fix.js` в принципе не касался, потому что тот живёт только в
10
+ браузере.
11
+
12
+ - Патчит `modules.fifo_lock.FIFOLock` **на уровне класса** (не инстанса —
13
+ `with queue_lock:` вызывает `type(queue_lock).__enter__`, инстанс-патч
14
+ `queue_lock.acquire = ...` для `with` был бы просто проигнорирован).
15
+ `queue_lock` — один и тот же объект, которым и кнопка Generate в UI
16
+ (`modules/ui.py:417`), и каждый `/sdapi/v1/*` роут (`modules/api/api.py`)
17
+ сериализуют вызовы. Если что-то держит его дольше `A1111_FIX_LOCK_WARN_SECONDS`
18
+ (по умолчанию 20с), в лог идёт: кто держит, с какой точки был взят лок,
19
+ сколько вызовов стоит в очереди, и живой stack trace потока-держателя —
20
+ то есть прямо строка кода, на которой всё встало.
21
+ - Оборачивает `modules.script_loading.load_module`, засекая время импорта
22
+ каждого расширения по отдельности (по полному пути, не по имени файла —
23
+ так `xy_grid.py` из разных расширений не сливаются в одну запись).
24
+ Если импорт идёт дольше `A1111_FIX_IMPORT_WARN_SECONDS` (по умолчанию
25
+ 15с) — предупреждение прямо во время зависания, а не только постфактум.
26
+ - Отдаёт то же самое через `GET /internal/a1111-fix/status` (JSON), и
27
+ печатает топ самых медленных импортов один раз в лог при старте.
28
+ - Всё обёрнуто в try/except на каждом уровне: если в другом форке A1111
29
+ что-то из этого не совпадает по сигнатуре — модуль один раз пишет об
30
+ этом в лог и отключает соответствующую часть, а не роняет запуск.
31
+
32
+ ## Исправлено: `modules/ui_gradio_extensions.py`
33
+
34
+ `_FIX_MARKERS` проверял вхождение подстроки `"fix.js"` в путь **любого**
35
+ скрипта — значит `polyfix.js` или `ie11-fix.js` из чужого расширения
36
+ ошибочно попадали в категорию `fix_extension`. Заменено на точное
37
+ сравнение имени папки расширения (`0000-a1111-fix`, с учётом
38
+ опционального `!`-префикса). Всё остальное в файле не тронуто — там
39
+ проблем не было.
40
+
41
+ ## Исправлено: `extensions/!0000-a1111-fix/metadata.ini`
42
+
43
+ - `canvas-zoom` / `sd-webui-canvas-zoom` не совпадали с реальной папкой
44
+ встроенного расширения в этом репозитории — она называется
45
+ `canvas-zoom-and-pan`. Directive молча ни на что не влиял. Добавлено
46
+ верное имя через синтаксис `alt1|alt2|alt3` (выбирает первую реально
47
+ установленную альтернативу, см. `modules/extensions.py:get_script_requirements`).
48
+ - Добавлена секция `[scripts/backend_watchdog.py]` с `Before`, перечисляющим
49
+ все папки из `extensions-builtin/` явно — по-другому раньше builtin'ов
50
+ забраться нельзя: A1111 полностью сканирует `extensions-builtin/`
51
+ раньше, чем вообще заглядывает в `extensions/`, независимо от имени
52
+ папки.
53
+ - Добавлены комментарии: почему `Name =` в `[Extension]` не влияет на
54
+ сопоставление (реально используется имя папки — это баг/особенность
55
+ самого A1111, не этого файла), и почему `Before` для `fix.js`
56
+ не строго обязателен для того, как сейчас устроены сами патчи.
57
+
58
+ ## Дополнено: `extensions/!0000-a1111-fix/javascript/fix.js`
59
+
60
+ Добавлена `window.__a1111FixBackendStatus()` — фетчит
61
+ `/internal/a1111-fix/status` и печатает единую картину (лок + импорты).
62
+ `window.__a1111FixReport()` теперь дополнительно вызывает её (fire-and-forget),
63
+ но остаётся **синхронной** и возвращает то же самое, что раньше — ничего
64
+ из существующего кода, читающего результат сразу, не сломано. Если
65
+ `backend_watchdog.py` не установлен или сервер недоступен — один
66
+ короткий лог, без исключений.
67
+
68
+ ## Проверено (не только прочитано — выполнено)
69
+
70
+ - `backend_watchdog.py`: 26/26 проверок в `run_test.py`, включая настоящий
71
+ `modules/fifo_lock.py` из этого репозитория (не мок) — подтверждён и
72
+ сам факт перехвата `with queue_lock:`, и то, что реальная взаимная
73
+ исключительность не сломана, и что watchdog реально ловит долгий hold
74
+ и зависший импорт вживую, до его завершения.
75
+ - `ui_gradio_extensions.py`: 7/7 проверок в `run_kind_test.py`, реальный
76
+ импорт файла через `importlib` (как это делает сам A1111), включая оба
77
+ ложных срабатывания (`polyfix.js`, `prefix.js`), которые раньше
78
+ давали `fix_extension`, а теперь — `user_extension`.
79
+ - `metadata.ini`: распарсен настоящим `configparser`, плюс воспроизведена
80
+ логика `get_script_requirements` — оба `Before`-блока резолвятся в
81
+ ожидаемые имена.
82
+ - `fix.js`: `node --check` на синтаксис + рантайм-smoke-тест в Node
83
+ (`fixjs_smoke_test.mjs`, 10/10), включая ветку "backend недоступен".
84
+
85
+ ## Как включить и на что смотреть
86
+
87
+ 1. Скопировать `extensions/!0000-a1111-fix/` и `modules/ui_gradio_extensions.py`
88
+ поверх своей установки, перезапустить webui.
89
+ 2. В консоли сервера при старте появится
90
+ `[a1111-fix][backend] backend watchdog active (...)` и, если что-то
91
+ долго импортировалось — рейтинг самых медленных расширений.
92
+ 3. Когда (если) снова случится зависание с потерей API — не гадать: либо
93
+ смотреть лог сервера (запись появится сама, watchdog-поток не ждёт
94
+ вашего запроса), либо дернуть `GET /internal/a1111-fix/status`, либо
95
+ в браузерной консоли `__a1111FixReport()` — там же теперь и backend-часть.
96
+ 4. Пороги подкручиваются без правки кода:
97
+ `A1111_FIX_LOCK_WARN_SECONDS`, `A1111_FIX_IMPORT_WARN_SECONDS`,
98
+ `A1111_FIX_WATCHDOG_INTERVAL`.
!0000-a1111-fix/javascript/fix.js ADDED
@@ -0,0 +1,506 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function () {
2
+ 'use strict';
3
+
4
+ const FIX_TAG = '[a1111-fix]';
5
+ const FIX_VERSION = 'v5.2-hardened';
6
+
7
+ if (window.__a1111_fix_v52_applied) {
8
+ console.warn(FIX_TAG, 'already applied, skipping duplicate load');
9
+ return;
10
+ }
11
+ window.__a1111_fix_v52_applied = true;
12
+
13
+ const wrappedCache = new WeakMap();
14
+ const crashLogOnce = new WeakMap();
15
+ const slowLogOnce = new WeakMap();
16
+
17
+ function markWeakOnce(store, callback, kind) {
18
+ if (typeof callback !== 'function') return false;
19
+ let kinds = store.get(callback);
20
+ if (!kinds) {
21
+ kinds = new Set();
22
+ store.set(callback, kinds);
23
+ }
24
+
25
+ if (kinds.has(kind)) return false;
26
+ kinds.add(kind);
27
+ return true;
28
+ }
29
+
30
+ function isWrappedForKind(callback, kind) {
31
+ return !!(
32
+ callback &&
33
+ typeof callback === 'function' &&
34
+ callback.__a1111_fix_wrapped === true &&
35
+ callback.__a1111_fix_kind === kind &&
36
+ typeof callback.__a1111_fix_original === 'function'
37
+ );
38
+ }
39
+
40
+ function getWrappedCallback(callback, kind, slowThresholdMs) {
41
+ if (typeof callback !== 'function') return callback;
42
+ if (isWrappedForKind(callback, kind)) return callback;
43
+
44
+ let sourceCallback = callback;
45
+ if (
46
+ callback.__a1111_fix_wrapped === true &&
47
+ callback.__a1111_fix_kind === kind &&
48
+ typeof callback.__a1111_fix_original === 'function'
49
+ ) {
50
+ sourceCallback = callback.__a1111_fix_original;
51
+ }
52
+
53
+ let perKind = wrappedCache.get(sourceCallback);
54
+ if (!perKind) {
55
+ perKind = Object.create(null);
56
+ wrappedCache.set(sourceCallback, perKind);
57
+ }
58
+
59
+ if (perKind[kind]) return perKind[kind];
60
+
61
+ const wrapped = function (arg) {
62
+ const t0 = performance.now();
63
+ try {
64
+ return sourceCallback(arg);
65
+ } catch (e) {
66
+ if (markWeakOnce(crashLogOnce, sourceCallback, kind)) {
67
+ console.error(FIX_TAG, kind + ' callback crashed:', sourceCallback.name || '(anonymous)', e);
68
+ }
69
+ } finally {
70
+ const elapsed = performance.now() - t0;
71
+ if (elapsed > slowThresholdMs && markWeakOnce(slowLogOnce, sourceCallback, kind)) {
72
+ console.warn(
73
+ FIX_TAG,
74
+ 'SLOW ' + kind + ': "' + (sourceCallback.name || '(anonymous)') + '" took ' + elapsed.toFixed(0) + 'ms'
75
+ );
76
+ }
77
+ }
78
+ };
79
+
80
+ wrapped.__a1111_fix_wrapped = true;
81
+ wrapped.__a1111_fix_original = sourceCallback;
82
+ wrapped.__a1111_fix_kind = kind;
83
+ perKind[kind] = wrapped;
84
+ return wrapped;
85
+ }
86
+
87
+ function wrapQueue(queue, kind, slowThresholdMs) {
88
+ if (!Array.isArray(queue)) return 0;
89
+
90
+ let wrappedCount = 0;
91
+ for (let i = 0; i < queue.length; i++) {
92
+ if (typeof queue[i] === 'function' && !isWrappedForKind(queue[i], kind)) {
93
+ queue[i] = getWrappedCallback(queue[i], kind, slowThresholdMs);
94
+ wrappedCount++;
95
+ }
96
+ }
97
+ return wrappedCount;
98
+ }
99
+
100
+ function patchQueuePush(queue, kind, slowThresholdMs) {
101
+ if (!Array.isArray(queue) || queue.__a1111_fix_push_patched) return false;
102
+
103
+ const originalPush = queue.push;
104
+ if (typeof originalPush !== 'function') return false;
105
+
106
+ queue.push = function (...items) {
107
+ const wrappedItems = items.map((item) =>
108
+ typeof item === 'function' ? getWrappedCallback(item, kind, slowThresholdMs) : item
109
+ );
110
+ return originalPush.apply(this, wrappedItems);
111
+ };
112
+ queue.__a1111_fix_push_patched = true;
113
+ return true;
114
+ }
115
+
116
+ function safeGradioApp() {
117
+ try {
118
+ if (typeof window.gradioApp === 'function') {
119
+ return window.gradioApp();
120
+ }
121
+ } catch (e) {
122
+ console.warn(FIX_TAG, 'gradioApp() failed while resolving accordion:', e);
123
+ }
124
+ return document;
125
+ }
126
+
127
+ function applyFix1() {
128
+ const originalSchedule = window.scheduleAfterUiUpdateCallbacks;
129
+ if (typeof originalSchedule !== 'function' ||
130
+ typeof window.executeCallbacks !== 'function' ||
131
+ !Array.isArray(window.uiAfterUpdateCallbacks)) {
132
+ console.warn(FIX_TAG, 'scheduleAfterUiUpdateCallbacks/executeCallbacks/uiAfterUpdateCallbacks not found, skipping FIX-1');
133
+ return;
134
+ }
135
+
136
+ if (originalSchedule.__a1111_fix_afterupdate_patched) {
137
+ console.warn(FIX_TAG, 'FIX-1 already patched, skipping duplicate apply');
138
+ return;
139
+ }
140
+
141
+ const MAX_WAIT_MS = 1000;
142
+ let maxWaitTimer = null;
143
+ let running = false;
144
+
145
+ function clearMaxWaitTimer() {
146
+ if (maxWaitTimer) {
147
+ clearTimeout(maxWaitTimer);
148
+ maxWaitTimer = null;
149
+ }
150
+ }
151
+
152
+ function runAfterUpdateNow() {
153
+ clearMaxWaitTimer();
154
+
155
+ const timeoutId = window.uiAfterUpdateTimeout;
156
+ if (timeoutId) {
157
+ clearTimeout(timeoutId);
158
+ window.uiAfterUpdateTimeout = null;
159
+ }
160
+
161
+ if (running) return;
162
+ running = true;
163
+ try {
164
+ if (typeof window.executeCallbacks === 'function') {
165
+ window.executeCallbacks(window.uiAfterUpdateCallbacks);
166
+ }
167
+ } finally {
168
+ running = false;
169
+ }
170
+ }
171
+
172
+ if (!window.executeCallbacks.__a1111_fix_afterupdate_guard) {
173
+ const previousExecuteCallbacks = window.executeCallbacks;
174
+ const guardedExecuteCallbacks = function (queue, arg) {
175
+ if (queue === window.uiAfterUpdateCallbacks) {
176
+ clearMaxWaitTimer();
177
+ }
178
+ return previousExecuteCallbacks(queue, arg);
179
+ };
180
+ guardedExecuteCallbacks.__a1111_fix_afterupdate_guard = true;
181
+ guardedExecuteCallbacks.__a1111_fix_afterupdate_guard_original = previousExecuteCallbacks;
182
+ window.executeCallbacks = guardedExecuteCallbacks;
183
+ }
184
+
185
+ const patchedSchedule = function (...args) {
186
+ const result = originalSchedule.apply(this, args);
187
+
188
+ if (!maxWaitTimer) {
189
+ maxWaitTimer = setTimeout(runAfterUpdateNow, MAX_WAIT_MS);
190
+ }
191
+
192
+ return result;
193
+ };
194
+ patchedSchedule.__a1111_fix_afterupdate_patched = true;
195
+ patchedSchedule.__a1111_fix_afterupdate_original = originalSchedule;
196
+ window.scheduleAfterUiUpdateCallbacks = patchedSchedule;
197
+
198
+ console.log(FIX_TAG, 'FIX-1: bounded after-update max-wait=' + MAX_WAIT_MS + 'ms applied (wrapper mode)');
199
+ }
200
+
201
+ function applyFix2() {
202
+ const configs = [
203
+ { queueName: 'uiLoadedCallbacks', registerName: 'onUiLoaded', slowThresholdMs: 100 },
204
+ { queueName: 'uiAfterUpdateCallbacks', registerName: 'onAfterUiUpdate', slowThresholdMs: 100 },
205
+ { queueName: 'uiUpdateCallbacks', registerName: 'onUiUpdate', slowThresholdMs: 50 },
206
+ { queueName: 'optionsChangedCallbacks', registerName: 'onOptionsChanged', slowThresholdMs: 50 },
207
+ { queueName: 'optionsAvailableCallbacks', registerName: 'onOptionsAvailable', slowThresholdMs: 50 },
208
+ { queueName: 'uiTabChangeCallbacks', registerName: 'onUiTabChange', slowThresholdMs: 50 },
209
+ ];
210
+
211
+ const activeConfigs = [];
212
+ const summary = [];
213
+
214
+ for (const cfg of configs) {
215
+ const queue = window[cfg.queueName];
216
+ const register = window[cfg.registerName];
217
+ if (!Array.isArray(queue) || typeof register !== 'function') {
218
+ continue;
219
+ }
220
+
221
+ const existingCount = wrapQueue(queue, cfg.registerName, cfg.slowThresholdMs);
222
+ patchQueuePush(queue, cfg.registerName, cfg.slowThresholdMs);
223
+
224
+ if (!register.__a1111_fix_register_patched) {
225
+ const originalRegister = register;
226
+ const patchedRegister = function (callback) {
227
+ return originalRegister(getWrappedCallback(callback, cfg.registerName, cfg.slowThresholdMs));
228
+ };
229
+ patchedRegister.__a1111_fix_register_patched = true;
230
+ patchedRegister.__a1111_fix_register_original = originalRegister;
231
+ window[cfg.registerName] = patchedRegister;
232
+ }
233
+
234
+ activeConfigs.push(cfg);
235
+ summary.push(cfg.registerName + '=' + existingCount);
236
+ }
237
+
238
+ if (activeConfigs.length === 0) {
239
+ console.warn(FIX_TAG, 'no callback queues found, skipping FIX-2');
240
+ return;
241
+ }
242
+
243
+ if (typeof window.executeCallbacks === 'function' && !window.executeCallbacks.__a1111_fix_patched) {
244
+ const originalExecuteCallbacks = window.executeCallbacks;
245
+ const patchedExecuteCallbacks = function (queue, arg) {
246
+ for (const cfg of activeConfigs) {
247
+ if (queue === window[cfg.queueName]) {
248
+ wrapQueue(queue, cfg.registerName, cfg.slowThresholdMs);
249
+ break;
250
+ }
251
+ }
252
+ return originalExecuteCallbacks(queue, arg);
253
+ };
254
+ patchedExecuteCallbacks.__a1111_fix_patched = true;
255
+ patchedExecuteCallbacks.__a1111_fix_original = originalExecuteCallbacks;
256
+ window.executeCallbacks = patchedExecuteCallbacks;
257
+ }
258
+
259
+ console.log(FIX_TAG, 'FIX-2: callback guards applied to', summary.join(', '));
260
+ }
261
+
262
+ function applyFix3() {
263
+ if (typeof window.inputAccordionChecked !== 'function') {
264
+ console.warn(FIX_TAG, 'inputAccordionChecked not found, skipping FIX-3');
265
+ return;
266
+ }
267
+
268
+ const warnedOnce = new Set();
269
+
270
+ function isRealAccordion(el) {
271
+ return !!(
272
+ el &&
273
+ el.visibleCheckbox instanceof HTMLInputElement &&
274
+ typeof el.onVisibleCheckboxChange === 'function'
275
+ );
276
+ }
277
+
278
+ function resolveRealAccordion(id) {
279
+ const app = safeGradioApp();
280
+ const direct = app.getElementById ? app.getElementById(id) : null;
281
+ if (isRealAccordion(direct)) return direct;
282
+
283
+ const dotted = app.getElementById ? app.getElementById('.' + id) : null;
284
+ if (isRealAccordion(dotted)) return dotted;
285
+
286
+ if (direct && typeof direct.querySelector === 'function') {
287
+ const nestedCheckbox = direct.querySelector('.input-accordion-checkbox');
288
+ if (nestedCheckbox) {
289
+ const parentAccordion = nestedCheckbox.closest('.input-accordion');
290
+ if (isRealAccordion(parentAccordion)) return parentAccordion;
291
+ }
292
+ }
293
+
294
+ return null;
295
+ }
296
+
297
+ window.inputAccordionChecked = function (id, checked) {
298
+ const realEl = resolveRealAccordion(id);
299
+ if (!realEl) {
300
+ if (!warnedOnce.has(id)) {
301
+ warnedOnce.add(id);
302
+ const app = safeGradioApp();
303
+ const direct = app.getElementById ? app.getElementById(id) : null;
304
+ const dotted = app.getElementById ? app.getElementById('.' + id) : null;
305
+ console.error(
306
+ FIX_TAG, 'FIX-3: cannot resolve accordion for id:', id,
307
+ '\n direct el:', direct,
308
+ '\n direct.visibleCheckbox:', direct && direct.visibleCheckbox,
309
+ '\n direct.visibleCheckbox instanceof HTMLInputElement:',
310
+ direct && (direct.visibleCheckbox instanceof HTMLInputElement),
311
+ '\n dotted el:', dotted
312
+ );
313
+ }
314
+ return;
315
+ }
316
+
317
+ realEl.visibleCheckbox.checked = checked;
318
+ realEl.onVisibleCheckboxChange();
319
+ };
320
+
321
+ console.log(FIX_TAG, 'FIX-3: resilient accordion resolver applied');
322
+ }
323
+
324
+ function applyFix4() {
325
+ window.__a1111ScriptRegistry = window.__a1111ScriptRegistry || {
326
+ mode: 'unknown',
327
+ scripts: {},
328
+ loaded: {},
329
+ errors: {},
330
+ injected_order: [],
331
+ };
332
+
333
+ function registerScriptElement(el) {
334
+ if (!el || el.tagName !== 'SCRIPT') return;
335
+ if (typeof window.__a1111RegisterScript === 'function') {
336
+ window.__a1111RegisterScript(el);
337
+ return;
338
+ }
339
+
340
+ const src = el.getAttribute('src') || '';
341
+ window.__a1111ScriptRegistry.scripts[src] = {
342
+ role: el.getAttribute('data-a1111-role') || 'unknown',
343
+ mode: el.getAttribute('data-a1111-mode') || 'unknown',
344
+ path: el.getAttribute('data-a1111-path') || src,
345
+ basedir: el.getAttribute('data-a1111-basedir') || '',
346
+ };
347
+ if (!window.__a1111ScriptRegistry.injected_order.includes(src)) {
348
+ window.__a1111ScriptRegistry.injected_order.push(src);
349
+ }
350
+ }
351
+
352
+ window.addEventListener('error', function (event) {
353
+ const target = event && event.target;
354
+ if (target && target.tagName === 'SCRIPT') {
355
+ registerScriptElement(target);
356
+ const src = target.getAttribute('src') || '';
357
+ window.__a1111ScriptRegistry.errors[src] = true;
358
+ return;
359
+ }
360
+
361
+ const filename = event && event.filename;
362
+ if (filename) {
363
+ console.error(FIX_TAG, 'window error from script:', filename, event.error || event.message || event);
364
+ }
365
+ }, true);
366
+
367
+ window.addEventListener('unhandledrejection', function (event) {
368
+ console.error(FIX_TAG, 'unhandled promise rejection:', event.reason || event);
369
+ });
370
+
371
+ function summarizeRegistry() {
372
+ const registry = window.__a1111ScriptRegistry || {};
373
+ const scripts = registry.scripts || {};
374
+ const loaded = registry.loaded || {};
375
+ const errors = registry.errors || {};
376
+ const order = registry.injected_order || [];
377
+
378
+ const ok = [];
379
+ const failed = [];
380
+ const pending = [];
381
+
382
+ for (const src of order.length ? order : Object.keys(scripts)) {
383
+ const meta = scripts[src] || {};
384
+ const item = {
385
+ src,
386
+ role: meta.role || 'unknown',
387
+ mode: meta.mode || 'unknown',
388
+ path: meta.path || src,
389
+ };
390
+
391
+ if (errors[src]) {
392
+ failed.push(item);
393
+ } else if (loaded[src]) {
394
+ ok.push(item);
395
+ } else {
396
+ pending.push(item);
397
+ }
398
+ }
399
+
400
+ return { ok, failed, pending, mode: registry.mode || 'unknown' };
401
+ }
402
+
403
+ function summarizeCallbacks() {
404
+ const groups = [
405
+ ['uiLoadedCallbacks', 'onUiLoaded'],
406
+ ['uiAfterUpdateCallbacks', 'onAfterUiUpdate'],
407
+ ['uiUpdateCallbacks', 'onUiUpdate'],
408
+ ['optionsChangedCallbacks', 'onOptionsChanged'],
409
+ ['optionsAvailableCallbacks', 'onOptionsAvailable'],
410
+ ['uiTabChangeCallbacks', 'onUiTabChange'],
411
+ ];
412
+ return groups.map(([queueName, registerName]) => {
413
+ const queue = window[queueName];
414
+ return {
415
+ queue: queueName,
416
+ register: registerName,
417
+ present: Array.isArray(queue),
418
+ count: Array.isArray(queue) ? queue.length : 0,
419
+ };
420
+ });
421
+ }
422
+
423
+ // Backend half of the picture. scripts/backend_watchdog.py exposes
424
+ // this endpoint if it's installed and managed to attach; if it's
425
+ // missing or unreachable this just logs one line and moves on, it
426
+ // never throws. Kept as a separate function (returns a Promise) so
427
+ // __a1111FixReport() below can stay synchronous -- existing callers
428
+ // that read its return value immediately, like the load-event
429
+ // handler a bit further down, keep working unchanged.
430
+ window.__a1111FixBackendStatus = function () {
431
+ return fetch('/internal/a1111-fix/status')
432
+ .then(function (resp) {
433
+ if (!resp.ok) throw new Error('HTTP ' + resp.status);
434
+ return resp.json();
435
+ })
436
+ .then(function (status) {
437
+ console.log(FIX_TAG, 'backend status:', status);
438
+ if (status.queue_lock && status.queue_lock.held) {
439
+ console.warn(
440
+ FIX_TAG,
441
+ 'queue_lock has been held for', status.queue_lock.held_for_seconds + 's',
442
+ 'by', status.queue_lock.owner_thread,
443
+ '(' + status.queue_lock.waiters_now, 'call(s) queued behind it) -- ' +
444
+ 'this blocks BOTH the UI Generate button and /sdapi/v1/* right now.'
445
+ );
446
+ }
447
+ if (status.currently_importing) {
448
+ console.warn(FIX_TAG, 'backend is still importing:', status.currently_importing);
449
+ }
450
+ if (Array.isArray(status.slowest_imports) && status.slowest_imports.length) {
451
+ console.table(status.slowest_imports);
452
+ }
453
+ return status;
454
+ })
455
+ .catch(function (err) {
456
+ // Not fatal -- scripts/backend_watchdog.py may not be
457
+ // installed, or the server may genuinely be unreachable
458
+ // right now, which is itself useful information.
459
+ console.log(FIX_TAG, 'backend status unavailable:', err.message);
460
+ return null;
461
+ });
462
+ };
463
+
464
+ window.__a1111FixReport = function () {
465
+ const report = summarizeRegistry();
466
+ const callbacks = summarizeCallbacks();
467
+ console.log(FIX_TAG, 'loader mode =', report.mode);
468
+ console.log(FIX_TAG, 'loaded scripts =', report.ok.length, 'failed =', report.failed.length, 'pending =', report.pending.length);
469
+ console.table(callbacks);
470
+
471
+ if (report.failed.length) {
472
+ console.warn(FIX_TAG, 'failed scripts:');
473
+ console.table(report.failed);
474
+ }
475
+ if (report.pending.length) {
476
+ console.warn(FIX_TAG, 'pending/unconfirmed scripts:');
477
+ console.table(report.pending);
478
+ }
479
+ if (!report.failed.length && !report.pending.length) {
480
+ console.log(FIX_TAG, 'all tracked scripts loaded cleanly');
481
+ }
482
+
483
+ window.__a1111FixBackendStatus();
484
+
485
+ return { ...report, callbacks };
486
+ };
487
+
488
+ window.addEventListener('load', function () {
489
+ setTimeout(function () {
490
+ const report = window.__a1111FixReport();
491
+ if (report.failed.length || report.pending.length) {
492
+ console.warn(FIX_TAG, 'FIX-4: loader diagnostics detected script issues; inspect __a1111FixReport() output');
493
+ }
494
+ }, 1500);
495
+ }, { once: true });
496
+
497
+ console.log(FIX_TAG, 'FIX-4: loader diagnostics ready');
498
+ }
499
+
500
+ applyFix1();
501
+ applyFix2();
502
+ applyFix3();
503
+ applyFix4();
504
+
505
+ console.log(FIX_TAG, FIX_VERSION, 'ready');
506
+ })();
!0000-a1111-fix/metadata.ini ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [Extension]
2
+ Name = 0000-a1111-fix
3
+
4
+ ; Notes:
5
+ ; - the real canonical name A1111 uses to match Before/After targets is
6
+ ; always the lowercased FOLDER name, not the "Name =" field above.
7
+ ; modules/extensions.py:ExtensionMetadata.__init__ reads "Name" and then,
8
+ ; a couple of lines later, unconditionally overwrites it with the folder
9
+ ; name -- so "Name =" here is documentation only, it has zero effect on
10
+ ; matching. Rename the FOLDER, not this line, if you ever need to change
11
+ ; how other extensions can reference this one.
12
+ ; - the "!0000-" folder prefix forces early alphabetical sort, but that only
13
+ ; wins against other entries inside extensions/. A1111 scans
14
+ ; extensions-builtin/ to completion before it ever looks inside
15
+ ; extensions/ (modules/extensions.py:list_extensions), so a builtin
16
+ ; extension is always earlier in the natural, unconstrained order no
17
+ ; matter what this folder is named. The Before= lines below are what
18
+ ; actually overrides that, via the topological sort in
19
+ ; modules/scripts.py:list_scripts().
20
+ ; - Before/After targets are matched by EXACT, case-insensitive folder name.
21
+ ; A non-matching name fails silently (no error, the edge is just never
22
+ ; created) -- verify names against the real extensions/ and
23
+ ; extensions-builtin/ directories instead of guessing. "canvas-zoom" used
24
+ ; to be a guess here and did not match the extension actually shipped in
25
+ ; this repo, which is named canvas-zoom-and-pan.
26
+ ; - "name1|name2|name3" (pipe, no surrounding spaces) picks whichever of the
27
+ ; alternatives is actually installed and ignores the rest -- see
28
+ ; modules/extensions.py:get_script_requirements. Used below for
29
+ ; third-party extensions whose exact folder name can vary by how the user
30
+ ; installed them.
31
+
32
+ ; javascript/fix.js
33
+ ; Kept for defense in depth, but not strictly load-bearing for FIX-1/2/3 as
34
+ ; currently written: they patch GLOBAL functions (scheduleAfterUiUpdateCallbacks,
35
+ ; executeCallbacks, inputAccordionChecked) by reassignment, and FIX-2
36
+ ; additionally re-wraps any callback queue right before it runs, regardless
37
+ ; of when things were registered. Both patterns only require fix.js to have
38
+ ; executed at all before DOMContentLoaded, which holds for any sync or defer
39
+ ; <script> tag no matter its position. Ordering would only matter if some
40
+ ; other extension captured a direct reference to the original function at
41
+ ; its own parse time instead of calling window.xxx(...) fresh on use --
42
+ ; uncommon, but the entries below cost nothing if unused.
43
+ [javascript/fix.js]
44
+ Before = sd-webui-tabs-extension|sd-webui-tabs-extension-main sd-webui-controlnet|sd-webui-controlnet-main canvas-zoom|sd-webui-canvas-zoom|canvas-zoom-and-pan
45
+
46
+ ; scripts/backend_watchdog.py
47
+ ; This one DOES need to run before other extensions' Python: it times each
48
+ ; extension's import as it happens, and it needs to be watching before an
49
+ ; import starts in order to catch a slow or stuck one live. Builtin
50
+ ; extensions are otherwise unreachable by any folder-naming trick (see
51
+ ; note above), so they are listed explicitly and exhaustively -- this is
52
+ ; every folder currently under extensions-builtin/ in this repo. Add to
53
+ ; this list if a future A1111 version ships additional builtin extensions.
54
+ [scripts/backend_watchdog.py]
55
+ Before = ldsr lora scunet swinir canvas-zoom-and-pan extra-options-section hypertile mobile postprocessing-for-training prompt-bracket-checker soft-inpainting
!0000-a1111-fix/scripts/backend_watchdog.py ADDED
@@ -0,0 +1,381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ !0000-a1111-fix / scripts/backend_watchdog.py
3
+
4
+ Backend-side counterpart to javascript/fix.js.
5
+
6
+ fix.js only ever sees the browser. It cannot detect or explain a genuine
7
+ server-side stall, because a stalled Python process cannot serve anything
8
+ to the browser in the first place -- there is nothing for client-side code
9
+ to observe. This module targets exactly that gap.
10
+
11
+ Verified against this exact codebase (stable-diffusion-webui, gradio==3.41.2):
12
+
13
+ 1. modules/call_queue.py defines ONE queue_lock = fifo_lock.FIFOLock().
14
+ modules/ui.py wraps every "Generate" button in wrap_gradio_gpu_call(),
15
+ which does `with queue_lock:`. modules/api/api.py stores the SAME lock
16
+ object as self.queue_lock and wraps txt2imgapi/img2imgapi/etc. in
17
+ `with self.queue_lock:`. Both the Gradio UI and every /sdapi/v1/* route
18
+ run in the same process on the same FastAPI app (webui.py) and
19
+ serialize on this exact lock. If anything holds it too long -- a stuck
20
+ extension callback in process()/postprocess()/on_cfg_denoiser, a
21
+ blocking network call, a deadlock on shared.state -- the UI and the
22
+ HTTP API go unresponsive together. That combination is what this
23
+ module watches for.
24
+
25
+ 2. modules/scripts.py:load_scripts() imports every extension script via
26
+ script_loading.load_module(scriptfile.path), sequentially, on the main
27
+ thread, before the server exists. A hang here explains "the whole
28
+ thing never even finishes starting" and can't be caught by anything
29
+ that only starts after app_started fires -- because app_started never
30
+ fires until this loop returns. This module times each import live and
31
+ can point at whichever one is currently stuck.
32
+
33
+ 3. Both are exposed as one small JSON endpoint so javascript/fix.js's
34
+ __a1111FixReport() can show one unified picture instead of guessing
35
+ which layer broke.
36
+
37
+ Correctness notes (read before touching the locking code):
38
+ - FIFOLock.__enter__ = acquire is bound at class-definition time, so
39
+ `with queue_lock:` calls type(queue_lock).__enter__, NOT an
40
+ instance attribute. Patching queue_lock.acquire on the instance would
41
+ silently do nothing for `with` statements. Everything here patches the
42
+ FIFOLock *class*, once, and __enter__/__exit__ explicitly.
43
+ - The wrapper never adds its own locking around the real acquire/release
44
+ call -- only plain attribute writes on a small dict-like state object,
45
+ which is safe under the GIL and can't introduce a new deadlock.
46
+ - Every entry point is wrapped in try/except; if this fork's internals
47
+ don't match what's verified above, this module logs once and steps
48
+ aside instead of blocking startup.
49
+ """
50
+
51
+ import io
52
+ import os
53
+ import sys
54
+ import time
55
+ import threading
56
+ import traceback
57
+
58
+ TAG = "[a1111-fix][backend]"
59
+
60
+
61
+ def _env_float(name, default):
62
+ try:
63
+ return float(os.environ.get(name, default))
64
+ except (TypeError, ValueError):
65
+ return default
66
+
67
+
68
+ LOCK_WARN_SECONDS = _env_float("A1111_FIX_LOCK_WARN_SECONDS", 20.0)
69
+ IMPORT_WARN_SECONDS = _env_float("A1111_FIX_IMPORT_WARN_SECONDS", 15.0)
70
+ WATCHDOG_INTERVAL_SECONDS = _env_float("A1111_FIX_WATCHDOG_INTERVAL", 2.0)
71
+
72
+
73
+ def _log(msg):
74
+ print(f"{TAG} {msg}", flush=True)
75
+
76
+
77
+ def _dump_thread_stack(thread_id, limit=40):
78
+ if thread_id is None:
79
+ return " (no thread id on record)"
80
+ frame = sys._current_frames().get(thread_id)
81
+ if frame is None:
82
+ return " (thread not found -- it likely finished between the check and the dump)"
83
+ buf = io.StringIO()
84
+ traceback.print_stack(frame, limit=limit, file=buf)
85
+ return buf.getvalue()
86
+
87
+
88
+ def _short_caller_site():
89
+ """Best-effort 'who is trying to acquire this lock' label, used when we
90
+ later report that the lock has been held too long."""
91
+ stack = traceback.extract_stack()
92
+ useful = [f for f in stack if "backend_watchdog.py" not in f.filename]
93
+ if not useful:
94
+ return "unknown"
95
+ f = useful[-1]
96
+ return f"{os.path.basename(f.filename)}:{f.lineno} in {f.name}"
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Part 1: queue_lock instrumentation
101
+ # ---------------------------------------------------------------------------
102
+
103
+ class _LockState:
104
+ def __init__(self):
105
+ self.held = False
106
+ self.instance = None
107
+ self.owner_thread_id = None
108
+ self.owner_thread_name = None
109
+ self.acquired_at = None
110
+ self.acquire_site = None
111
+ self.waiters_at_acquire = 0
112
+ self.last_warned_at = 0.0
113
+ self.bookkeeping_lock = threading.Lock() # protects this object only, never queue_lock itself
114
+
115
+
116
+ _lock_state = _LockState()
117
+
118
+
119
+ def _patch_queue_lock():
120
+ try:
121
+ from modules import fifo_lock
122
+ except Exception as e:
123
+ _log(f"queue_lock instrumentation skipped: modules.fifo_lock not importable ({e})")
124
+ return False
125
+
126
+ if getattr(fifo_lock.FIFOLock, "_a1111_fix_patched", False):
127
+ return True # already patched (e.g. re-exec on UI reload)
128
+
129
+ original_acquire = fifo_lock.FIFOLock.acquire
130
+ original_release = fifo_lock.FIFOLock.release
131
+
132
+ def patched_acquire(self, blocking=True):
133
+ site = _short_caller_site()
134
+ ok = original_acquire(self, blocking)
135
+ if ok:
136
+ with _lock_state.bookkeeping_lock:
137
+ _lock_state.held = True
138
+ _lock_state.instance = self
139
+ _lock_state.owner_thread_id = threading.get_ident()
140
+ _lock_state.owner_thread_name = threading.current_thread().name
141
+ _lock_state.acquired_at = time.time()
142
+ _lock_state.acquire_site = site
143
+ _lock_state.waiters_at_acquire = len(getattr(self, "_pending_threads", ()) or ())
144
+ return ok
145
+
146
+ def patched_release(self):
147
+ try:
148
+ original_release(self)
149
+ finally:
150
+ with _lock_state.bookkeeping_lock:
151
+ _lock_state.held = False
152
+ _lock_state.instance = None
153
+ _lock_state.owner_thread_id = None
154
+ _lock_state.owner_thread_name = None
155
+ _lock_state.acquired_at = None
156
+ _lock_state.acquire_site = None
157
+
158
+ fifo_lock.FIFOLock.acquire = patched_acquire
159
+ fifo_lock.FIFOLock.release = patched_release
160
+ fifo_lock.FIFOLock.__enter__ = patched_acquire
161
+ fifo_lock.FIFOLock.__exit__ = lambda self, t, v, tb: patched_release(self)
162
+ fifo_lock.FIFOLock._a1111_fix_patched = True
163
+ _log("queue_lock instrumented (modules.fifo_lock.FIFOLock patched at class level)")
164
+ return True
165
+
166
+
167
+ def _lock_status_snapshot():
168
+ with _lock_state.bookkeeping_lock:
169
+ if not _lock_state.held:
170
+ return {"held": False}
171
+ held_for = time.time() - _lock_state.acquired_at
172
+ live_waiters = _lock_state.waiters_at_acquire
173
+ inst = _lock_state.instance
174
+ if inst is not None:
175
+ live_waiters = len(getattr(inst, "_pending_threads", ()) or ())
176
+ return {
177
+ "held": True,
178
+ "held_for_seconds": round(held_for, 1),
179
+ "owner_thread": _lock_state.owner_thread_name,
180
+ "acquired_at_site": _lock_state.acquire_site,
181
+ "waiters_now": live_waiters,
182
+ }
183
+
184
+
185
+ def _check_lock_watchdog(now):
186
+ with _lock_state.bookkeeping_lock:
187
+ if not _lock_state.held:
188
+ return
189
+ held_for = now - _lock_state.acquired_at
190
+ if held_for < LOCK_WARN_SECONDS:
191
+ return
192
+ if now - _lock_state.last_warned_at < LOCK_WARN_SECONDS:
193
+ return # re-warn periodically, not on every tick
194
+ _lock_state.last_warned_at = now
195
+ owner_id = _lock_state.owner_thread_id
196
+ owner_name = _lock_state.owner_thread_name
197
+ site = _lock_state.acquire_site
198
+ inst = _lock_state.instance
199
+ waiters = len(getattr(inst, "_pending_threads", ()) or ()) if inst is not None else "?"
200
+
201
+ stack_text = _dump_thread_stack(owner_id)
202
+ _log(
203
+ f"queue_lock has been held for {held_for:.1f}s by thread '{owner_name}' "
204
+ f"(acquired from {site}); {waiters} other call(s) queued behind it.\n"
205
+ f"This is the SAME lock used by the UI Generate button AND every "
206
+ f"/sdapi/v1/* route, so both are blocked right now behind whatever "
207
+ f"this thread is doing:\n{stack_text}"
208
+ )
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # Part 2: per-extension import timing
213
+ # ---------------------------------------------------------------------------
214
+
215
+ class _ImportState:
216
+ def __init__(self):
217
+ self.current_path = None
218
+ self.current_started_at = None
219
+ self.records = [] # list of (path, seconds)
220
+ self.lock = threading.Lock()
221
+
222
+
223
+ _import_state = _ImportState()
224
+
225
+
226
+ def _extension_label_for_path(path):
227
+ """'/srv/webui/extensions/sd-webui-controlnet/scripts/controlnet.py'
228
+ -> 'sd-webui-controlnet/controlnet.py'. Falls back to the bare filename
229
+ for anything outside extensions/extensions-builtin (core scripts)."""
230
+ if not path:
231
+ return None
232
+ norm = path.replace("\\", "/")
233
+ for marker in ("/extensions-builtin/", "/extensions/"):
234
+ if marker in norm:
235
+ tail = norm.split(marker, 1)[1]
236
+ parts = tail.split("/")
237
+ if len(parts) >= 2:
238
+ return f"{parts[0]}/{parts[-1]}"
239
+ return tail
240
+ return os.path.basename(norm)
241
+
242
+
243
+ def _patch_script_loading():
244
+ try:
245
+ from modules import script_loading
246
+ except Exception as e:
247
+ _log(f"import timing skipped: modules.script_loading not importable ({e})")
248
+ return False
249
+
250
+ if getattr(script_loading, "_a1111_fix_patched", False):
251
+ return True
252
+
253
+ original_load_module = script_loading.load_module
254
+
255
+ def timed_load_module(path):
256
+ with _import_state.lock:
257
+ _import_state.current_path = path
258
+ _import_state.current_started_at = time.time()
259
+ t0 = time.time()
260
+ try:
261
+ return original_load_module(path)
262
+ finally:
263
+ elapsed = time.time() - t0
264
+ with _import_state.lock:
265
+ _import_state.current_path = None
266
+ _import_state.current_started_at = None
267
+ _import_state.records.append((path, elapsed))
268
+ if elapsed >= IMPORT_WARN_SECONDS:
269
+ _log(f"slow import: {_extension_label_for_path(path)} took {elapsed:.1f}s")
270
+
271
+ script_loading.load_module = timed_load_module
272
+ script_loading._a1111_fix_patched = True
273
+ _log("import timing instrumented (modules.script_loading.load_module wrapped)")
274
+ return True
275
+
276
+
277
+ def _check_import_watchdog(now):
278
+ with _import_state.lock:
279
+ path = _import_state.current_path
280
+ started = _import_state.current_started_at
281
+ if path is None or started is None:
282
+ return
283
+ elapsed = now - started
284
+ if elapsed < IMPORT_WARN_SECONDS:
285
+ return
286
+ stack_text = _dump_thread_stack(threading.main_thread().ident)
287
+ _log(
288
+ f"still importing {_extension_label_for_path(path)} after {elapsed:.1f}s "
289
+ f"-- main thread is currently here:\n{stack_text}"
290
+ )
291
+
292
+
293
+ def _slowest_imports(top_n=10):
294
+ with _import_state.lock:
295
+ records = list(_import_state.records)
296
+ records.sort(key=lambda r: r[1], reverse=True)
297
+ out, seen = [], set()
298
+ for p, s in records:
299
+ label = _extension_label_for_path(p)
300
+ if label in seen or s < 0.05:
301
+ continue
302
+ seen.add(label)
303
+ out.append({"extension": label, "seconds": round(s, 2)})
304
+ if len(out) >= top_n:
305
+ break
306
+ return out
307
+
308
+
309
+ # ---------------------------------------------------------------------------
310
+ # Part 3: watchdog thread + status endpoint + startup summary
311
+ # ---------------------------------------------------------------------------
312
+
313
+ def _watchdog_loop():
314
+ while True:
315
+ try:
316
+ now = time.time()
317
+ _check_lock_watchdog(now)
318
+ _check_import_watchdog(now)
319
+ except Exception:
320
+ _log("watchdog tick raised an exception (continuing):\n" + traceback.format_exc())
321
+ time.sleep(WATCHDOG_INTERVAL_SECONDS)
322
+
323
+
324
+ _route_registered = False
325
+
326
+
327
+ def _register_status_route(app):
328
+ global _route_registered
329
+ if _route_registered or app is None:
330
+ return
331
+ try:
332
+ @app.get("/internal/a1111-fix/status")
333
+ def _a1111_fix_status():
334
+ return {
335
+ "queue_lock": _lock_status_snapshot(),
336
+ "currently_importing": _extension_label_for_path(_import_state.current_path),
337
+ "slowest_imports": _slowest_imports(),
338
+ }
339
+ _route_registered = True
340
+ _log("status endpoint ready: GET /internal/a1111-fix/status")
341
+ except Exception:
342
+ _log("could not register status endpoint:\n" + traceback.format_exc())
343
+
344
+
345
+ def _print_startup_summary():
346
+ slow = _slowest_imports(top_n=10)
347
+ if not slow:
348
+ return
349
+ lines = "\n".join(f" {e['seconds']:>6.2f}s {e['extension']}" for e in slow)
350
+ _log(f"slowest extension imports this session:\n{lines}")
351
+
352
+
353
+ def _on_app_started(demo, app):
354
+ _register_status_route(app)
355
+ _print_startup_summary()
356
+
357
+
358
+ def _main():
359
+ _patch_queue_lock()
360
+ _patch_script_loading()
361
+
362
+ watchdog_thread = threading.Thread(target=_watchdog_loop, name="a1111-fix-watchdog", daemon=True)
363
+ watchdog_thread.start()
364
+
365
+ try:
366
+ from modules import script_callbacks
367
+ script_callbacks.on_app_started(_on_app_started)
368
+ except Exception:
369
+ _log("could not register on_app_started callback:\n" + traceback.format_exc())
370
+
371
+ _log(
372
+ f"backend watchdog active "
373
+ f"(lock warn: {LOCK_WARN_SECONDS}s, import warn: {IMPORT_WARN_SECONDS}s, "
374
+ f"tick: {WATCHDOG_INTERVAL_SECONDS}s)"
375
+ )
376
+
377
+
378
+ try:
379
+ _main()
380
+ except Exception:
381
+ _log("failed to initialize -- backend diagnostics disabled:\n" + traceback.format_exc())