diff --git a/extensions-builtin/sdnext-kanvas b/extensions-builtin/sdnext-kanvas index 83dbace79..86701f393 160000 --- a/extensions-builtin/sdnext-kanvas +++ b/extensions-builtin/sdnext-kanvas @@ -1 +1 @@ -Subproject commit 83dbace796d1a682b484f3499fc9475c4d27e00d +Subproject commit 86701f3933e4f683719ad32de0420ba4a51f3a7c diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 753ee8d3e..183524444 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 753ee8d3efa5b7947adb19d9c19dcc8c3a5e7190 +Subproject commit 183524444e3c700775125e558db11db3625345fe diff --git a/javascript/loader.js b/javascript/loader.js index 597854fd1..3abd49a6a 100644 --- a/javascript/loader.js +++ b/javascript/loader.js @@ -1,4 +1,5 @@ const appStartTime = performance.now(); +let monitorLogActive = false; async function preloadImages() { const dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; @@ -26,6 +27,39 @@ async function preloadImages() { } } +function joinArgs(messages) { + let output = ''; + for (let i = 0; i < messages.length; i++) { + let arg = messages[i]; + if (arg === undefined) arg = 'undefined'; + if (arg === null) arg = 'null'; + output += ' '; + if (typeof arg === 'object') output += JSON.stringify(arg).replace(/["]+/g, ''); + else output += arg; + } + return output; +} + +async function monitorLog() { + if (window.logBufferDirty) { + window.logBufferDirty = false; + const maxLines = 5; // print last n logs from ring buffer to splash-log + const lines = []; + // print last 5 logs from ring buffer in reverse order + for (let i = window.logRingBuffer.length - 1; i >= Math.max(0, window.logRingBuffer.length - maxLines); i--) { + const logEntry = window.logRingBuffer[i]; + let color = 'white'; + if (logEntry.type === 'error') color = 'palevioletred'; + else if (logEntry.type === 'debug') color = 'gray'; + const html = `
${logEntry.ts}   ${joinArgs(logEntry.msg)}
`; + lines.push(html); + } + const splashLogEl = document.getElementById('splashLog'); + if (splashLogEl) splashLogEl.innerHTML = lines.join(''); + } + if (monitorLogActive) setTimeout(monitorLog, 1000); +} + async function removeSplash() { const splash = document.getElementById('splash'); if (splash) splash.remove(); @@ -33,6 +67,7 @@ async function removeSplash() { const t = Math.round(performance.now() - appStartTime); log('startupTime', t); xhrPost(`${window.api}/log`, { message: `ready time=${t}` }); + monitorLogActive = false; } async function createSplash() { @@ -43,6 +78,7 @@ async function createSplash() {
+
`; document.body.insertAdjacentHTML('beforeend', splash); const ok = await preloadImages(); @@ -51,14 +87,23 @@ async function createSplash() { return; } const imgEl = `
`; - document.getElementById('splash').insertAdjacentHTML('afterbegin', imgEl); - authFetch(`${window.api}/motd`) + const splashEl = document.getElementById('splash'); + if (splashEl) splashEl.insertAdjacentHTML('afterbegin', imgEl); + + monitorLogActive = true; + monitorLog(); + + await authFetch(`${window.api}/motd`) .then((res) => res.text()) .then((text) => { + const clean = text.replace(/["]+/g, ''); + log('getMOTD', clean); const motdEl = document.getElementById('motd'); - if (motdEl) motdEl.innerHTML = text.replace(/["]+/g, ''); + if (motdEl) motdEl.innerHTML = clean; }) .catch((err) => error(`getMOTD: ${err}`)); + + log('loadGradioUi'); } window.onload = createSplash; diff --git a/javascript/logger.js b/javascript/logger.js index 739afbb73..177c070ad 100644 --- a/javascript/logger.js +++ b/javascript/logger.js @@ -1,3 +1,12 @@ +window.logRingBuffer = []; +window.logBufferDirty = false; + +const logBuffer = (ts, type, msg) => { + window.logRingBuffer.push({ ts, type, msg }); + if (window.logRingBuffer.length > 10) window.logRingBuffer.shift(); + window.logBufferDirty = true; +}; + const scrollBottom = async (el) => { const lastChild = el.lastElementChild; if (lastChild) lastChild.scrollIntoView({ behavior: 'smooth' }); @@ -11,6 +20,7 @@ const log = async (...msg) => { scrollBottom(window.logger); } console.log(ts, ...msg); + logBuffer(ts, 'log', msg); }; const debug = async (...msg) => { @@ -21,6 +31,7 @@ const debug = async (...msg) => { scrollBottom(window.logger); } console.debug(ts, ...msg); + logBuffer(ts, 'debug', msg); }; const error = async (...msg) => { @@ -31,6 +42,7 @@ const error = async (...msg) => { scrollBottom(window.logger); } console.error(ts, ...msg); + logBuffer(ts, 'error', msg); // const txt = msg.join(' '); // if (!txt.includes('asctime') && !txt.includes('xhr.')) xhrPost('/sdapi/v1/log', { error: txt }); // eslint-disable-line no-use-before-define }; diff --git a/javascript/script.js b/javascript/script.js index 994880a45..399ada25f 100644 --- a/javascript/script.js +++ b/javascript/script.js @@ -67,7 +67,10 @@ function executeCallbacks(queue, arg) { for (const callback of queue) { if (!callback) continue; try { + const t0 = performance.now(); callback(arg); + const t1 = performance.now(); + if (t1 - t0 > 250) log('callbackSlow', callback.name || callback, `time=${Math.round(t1 - t0)}`); } catch (e) { error(`executeCallbacks: ${callback} ${e}`); } @@ -83,17 +86,21 @@ function scheduleAfterUiUpdateCallbacks() { let executedOnLoaded = false; const ignoreElements = ['logMonitorData', 'logWarnings', 'logErrors', 'tooltip-container', 'logger']; +const ignoreElementsSet = new Set(ignoreElements); const ignoreClasses = ['wrap']; let mutationTimer = null; let validMutations = []; + async function mutationCallback(mutations) { - let newMutations = mutations; - if (newMutations.length > 0) newMutations = newMutations.filter((m) => m.target.nodeName !== 'LABEL'); - if (newMutations.length > 0) newMutations = newMutations.filter((m) => ignoreElements.indexOf(m.target.id) === -1); - if (newMutations.length > 0) newMutations = newMutations.filter((m) => m.target.id !== 'logWarnings' && m.target.id !== 'logErrors'); - if (newMutations.length > 0) newMutations = newMutations.filter((m) => !m.target.classList?.contains('wrap')); - if (newMutations.length > 0) validMutations = validMutations.concat(newMutations); + if (mutations.length <= 0) return; + for (const mutation of mutations) { + const target = mutation.target; + if (target.nodeName === 'LABEL') continue; + if (ignoreElementsSet.has(target.id)) continue; + if (target.classList?.contains(ignoreClasses[0])) continue; + validMutations.push(mutation); + } if (validMutations.length < 1) return; if (mutationTimer) clearTimeout(mutationTimer); @@ -113,12 +120,13 @@ async function mutationCallback(mutations) { } validMutations = []; mutationTimer = null; - }, 50); + }, 100); } document.addEventListener('DOMContentLoaded', () => { + log('DOMContentLoaded'); const mutationObserver = new MutationObserver(mutationCallback); - mutationObserver.observe(gradioApp(), { childList: true, subtree: true }); + mutationObserver.observe(gradioApp(), { childList: true, subtree: true, attributes: false }); }); /** diff --git a/javascript/settings.js b/javascript/settings.js index 9aa22c626..798e4ac82 100644 --- a/javascript/settings.js +++ b/javascript/settings.js @@ -15,7 +15,7 @@ function monitorOption(option, callback) { monitoredOpts.push({ [option]: callback }); } -const AppyOpts = [ +const AppyOpts = [ // monitored opts { compact_view: (val, old) => toggleCompact(val, old) }, { gradio_theme: (val, old) => setTheme(val, old) }, { font_size: (val, old) => setFontSize(val, old) }, @@ -38,7 +38,12 @@ async function updateOpts(json_string) { for (const op of AppyOpts) { const [key, callback] = Object.entries(op)[0]; - if (callback) callback(new_opts[key], opts[key]); + if (callback) { + const t3 = performance.now(); + callback(new_opts[key], opts[key]); + const t4 = performance.now(); + if (t4 - t3 > 100) debug('AppyOptSlow', key, `time=${Math.round(t4 - t3)}`); + } } const t2 = performance.now(); @@ -109,7 +114,7 @@ function updateAllOpts() { return true; } -onAfterUiUpdate(async () => { +async function onAfterUiUpdateCallback() { if (!updateAllOpts()) return; const json_elem = gradioApp().getElementById('settings_json'); const textarea = json_elem.querySelector('textarea'); @@ -146,15 +151,19 @@ onAfterUiUpdate(async () => { }); }, 250); }; -}); +} -onOptionsChanged(() => { +onAfterUiUpdate(onAfterUiUpdateCallback); + +async function onOptionsChangedCallback() { const setting_elems = gradioApp().querySelectorAll('#settings [id^="setting_"]'); setting_elems.forEach((elem) => { const setting_name = elem.id.replace('setting_', ''); markIfModified(setting_name, opts[setting_name]); }); -}); +} + +onOptionsChanged(onOptionsChangedCallback); async function initModels() { const warn = () => ` diff --git a/javascript/startup.js b/javascript/startup.js index a51a0272d..7168b0a9d 100644 --- a/javascript/startup.js +++ b/javascript/startup.js @@ -18,15 +18,15 @@ async function waitForOpts() { break; } } - await sleep(50); + await sleep(100); t1 = performance.now(); } } async function initStartup() { const t0 = performance.now(); - log('gradio', `time=${Math.round(t0 - appStartTime)}`); - log('initStartup'); + log('initGradio', `time=${Math.round(t0 - appStartTime)}`); + log('initUi'); if (window.setupLogger) await setupLogger(); // all items here are non-blocking async calls @@ -54,22 +54,23 @@ async function initStartup() { } setRefreshInterval(); executeCallbacks(uiReadyCallbacks); - initLogMonitor(); setupExtraNetworks(); // optinally wait for modern ui if (window.waitForUiReady) await waitForUiReady(); initAutocomplete(); monitorConnection(); - removeSplash(); // post startup tasks that may take longer but are not critical showNetworks(); setHints(); applyStyles(); initIndexDB(); + initLogMonitor(); t1 = performance.now(); log('initStartup', Math.round(1000 * (t1 - t0) / 1000000)); + + removeSplash(); } onUiLoaded(initStartup); diff --git a/javascript/ui.js b/javascript/ui.js index 913cb0fc6..9a939d282 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -4,6 +4,10 @@ window.titles = {}; let tabSelected = ''; let txt2img_textarea; let img2img_textarea; +let fontSizeApplyRaf = 0; +let pendingFontSize = null; +let appliedFontSize = null; +let cachedGradioRoot = null; const wait_time = 800; const token_timeouts = {}; let uiLoaded = false; @@ -132,18 +136,34 @@ async function setTheme(val, old) { } function setFontSize(val, old) { - const size = val || opts.font_size; - if (size === old) return; - document.documentElement.style.setProperty('--font-size', `${size}px`); - gradioApp().style.setProperty('--font-size', `${size}px`); - gradioApp().style.setProperty('--text-xxs', `${size - 3}px`); - gradioApp().style.setProperty('--text-xs', `${size - 2}px`); - gradioApp().style.setProperty('--text-sm', `${size - 1}px`); - gradioApp().style.setProperty('--text-md', `${size}px`); - gradioApp().style.setProperty('--text-lg', `${size + 1}px`); - gradioApp().style.setProperty('--text-xl', `${size + 2}px`); - gradioApp().style.setProperty('--text-xxl', `${size + 3}px`); - log('setFontSize', size); + const size = Number(val || opts.font_size); + if (!Number.isFinite(size)) return; + if (size === old || size === appliedFontSize || size === pendingFontSize) return; + pendingFontSize = size; + if (fontSizeApplyRaf) return; + + fontSizeApplyRaf = requestAnimationFrame(() => { + const t0 = performance.now(); + fontSizeApplyRaf = 0; + const nextSize = pendingFontSize; + pendingFontSize = null; + if (!Number.isFinite(nextSize) || nextSize === appliedFontSize) return; + + cachedGradioRoot = cachedGradioRoot || gradioApp(); + const rootStyle = cachedGradioRoot.style; + document.documentElement.style.setProperty('--font-size', `${nextSize}px`); + rootStyle.setProperty('--font-size', `${nextSize}px`); + rootStyle.setProperty('--text-xxs', `${nextSize - 3}px`); + rootStyle.setProperty('--text-xs', `${nextSize - 2}px`); + rootStyle.setProperty('--text-sm', `${nextSize - 1}px`); + rootStyle.setProperty('--text-md', `${nextSize}px`); + rootStyle.setProperty('--text-lg', `${nextSize + 1}px`); + rootStyle.setProperty('--text-xl', `${nextSize + 2}px`); + rootStyle.setProperty('--text-xxl', `${nextSize + 3}px`); + appliedFontSize = nextSize; + const t1 = performance.now(); + log('setFontSize', nextSize, `time=${Math.round(t1 - t0)}`); + }); } function switchToTab(tab) { @@ -350,6 +370,28 @@ function clearPrompts(prompt, negative_prompt) { } const promptTokenCountUpdateFuncs = {}; +const registeredPromptTextareas = new WeakSet(); +const registeredPromptIds = new Set(); +const pendingCounterPlacement = new Set(); +const promptRegistrationConfig = [ + ['txt2img_prompt', 'txt2img_token_counter', 'txt2img_token_button'], + ['txt2img_neg_prompt', 'txt2img_negative_token_counter', 'txt2img_negative_token_button'], + ['img2img_prompt', 'img2img_token_counter', 'img2img_token_button'], + ['img2img_neg_prompt', 'img2img_negative_token_counter', 'img2img_negative_token_button'], + ['control_prompt', 'control_token_counter', 'control_token_button'], + ['control_neg_prompt', 'control_negative_token_counter', 'control_negative_token_button'], +]; +let promptRegistrationRaf = 0; +let promptRegistrationCursor = 0; +let promptRegistrationInProgress = false; + +function scheduleIdleUI(task) { + if (typeof window.requestIdleCallback === 'function') { + window.requestIdleCallback(task, { timeout: 500 }); + } else { + setTimeout(task, 0); + } +} function recalculatePromptTokens(name) { if (promptTokenCountUpdateFuncs[name]) { @@ -427,30 +469,89 @@ function sortUIElements() { log('sortUIElements'); } -onAfterUiUpdate(async () => { - async function registerTextarea(id, id_counter, id_button) { - const prompt = gradioApp().getElementById(id); - if (!prompt) return; - const counter = gradioApp().getElementById(id_counter); - const localTextarea = gradioApp().querySelector(`#${id} > label > textarea`); - if (counter.parentElement === prompt.parentElement) return; - prompt.parentElement.insertBefore(counter, prompt); - prompt.parentElement.style.position = 'relative'; - promptTokenCountUpdateFuncs[id] = () => { update_token_counter(id_button); }; - localTextarea.addEventListener('input', promptTokenCountUpdateFuncs[id]); - } - +function registerTextareaCallback() { // sortUIElements(); if (promptsInitialized) return; - log('initPrompts'); - registerTextarea('txt2img_prompt', 'txt2img_token_counter', 'txt2img_token_button'); - registerTextarea('txt2img_neg_prompt', 'txt2img_negative_token_counter', 'txt2img_negative_token_button'); - registerTextarea('img2img_prompt', 'img2img_token_counter', 'img2img_token_button'); - registerTextarea('img2img_neg_prompt', 'img2img_negative_token_counter', 'img2img_negative_token_button'); - registerTextarea('control_prompt', 'control_token_counter', 'control_token_button'); - registerTextarea('control_neg_prompt', 'control_negative_token_counter', 'control_negative_token_button'); - promptsInitialized = true; -}); + if (promptRegistrationInProgress) return; + + const app = gradioApp(); + if (!app) return; + + const registerTextarea = (id, id_counter, id_button) => { + const prompt = app.getElementById(id); + const counter = app.getElementById(id_counter); + const localTextarea = prompt?.querySelector('label > textarea'); + if (!prompt || !counter || !localTextarea || !prompt.parentElement) return false; + + const promptParent = prompt.parentElement; + const needsCounterPlacement = counter.parentElement !== promptParent || counter.nextElementSibling !== prompt; + if (needsCounterPlacement && !pendingCounterPlacement.has(id)) { + pendingCounterPlacement.add(id); + scheduleIdleUI(() => { + pendingCounterPlacement.delete(id); + const currentPrompt = app.getElementById(id); + const currentCounter = app.getElementById(id_counter); + if (!currentPrompt || !currentCounter || !currentPrompt.parentElement) return; + const currentParent = currentPrompt.parentElement; + if (currentCounter.parentElement !== currentParent || currentCounter.nextElementSibling !== currentPrompt) { + currentParent.insertBefore(currentCounter, currentPrompt); + } + if (currentParent.style.position !== 'relative') { + currentParent.style.position = 'relative'; + } + }); + } + + if (!promptTokenCountUpdateFuncs[id]) promptTokenCountUpdateFuncs[id] = () => { update_token_counter(id_button); }; + if (!registeredPromptTextareas.has(localTextarea)) { + localTextarea.addEventListener('input', promptTokenCountUpdateFuncs[id]); + registeredPromptTextareas.add(localTextarea); + } + return true; + }; + + const runPromptRegistrationStep = () => { + promptRegistrationRaf = 0; + const total = promptRegistrationConfig.length; + let cfg = null; + + // Process one prompt registration per frame to avoid long blocking work. + for (let attempts = 0; attempts < total; attempts += 1) { + const nextCfg = promptRegistrationConfig[promptRegistrationCursor]; + promptRegistrationCursor = (promptRegistrationCursor + 1) % total; + const [id] = nextCfg; + if (!registeredPromptIds.has(id)) { + cfg = nextCfg; + break; + } + } + + if (cfg) { + const [id] = cfg; + if (registerTextarea(...cfg)) { + registeredPromptIds.add(id); + } else { + // Prompt not available yet, retry on next onAfterUiUpdate callback. + promptRegistrationInProgress = false; + return; + } + } + + promptsInitialized = registeredPromptIds.size === total; + if (promptsInitialized) { + promptRegistrationInProgress = false; + log('initPrompts', registeredPromptIds.size); + return; + } + + promptRegistrationRaf = requestAnimationFrame(runPromptRegistrationStep); + }; + + promptRegistrationInProgress = true; + promptRegistrationRaf = requestAnimationFrame(runPromptRegistrationStep); +} + +onAfterUiUpdate(registerTextareaCallback); function update_txt2img_tokens(...args) { update_token_counter('txt2img_token_button'); @@ -564,7 +665,7 @@ function createThemeElement() { return el; } -function toggleCompact(val, old) { +async function toggleCompact(val, old) { if (val === old) return; log('toggleCompact', val, old); if (val) { diff --git a/modules/ui_definitions.py b/modules/ui_definitions.py index 4b573296d..e01eb72be 100644 --- a/modules/ui_definitions.py +++ b/modules/ui_definitions.py @@ -526,6 +526,23 @@ def create_settings(cmd_opts): "compact_view": OptionInfo(False, "Compact view"), "ui_columns": OptionInfo(4, "Gallery view columns", gr.Slider, {"minimum": 1, "maximum": 8, "step": 1}), + 'uiux_separator_appearance': OptionInfo("

Appearance

", "", gr.HTML), + "uiux_grid_image_size": OptionInfo(150, "Grid image size", gr.Slider, {"minimum": 64, "maximum": 1024, "step": 1}), + "uiux_panel_min_width": OptionInfo(35, "Panel minimum width", gr.Number), + "uiux_hide_legacy": OptionInfo(True, "Hide legacy tabs"), + "uiux_persist_layout": OptionInfo(True, "Persist UI layout"), + "uiux_no_slider_layout": OptionInfo(False, "Hide input range sliders"), + "uiux_show_labels_aside": OptionInfo(False, "Show labels for aside tabs"), + "uiux_show_labels_main": OptionInfo(False, "Show labels for main tabs"), + "uiux_show_labels_tabs": OptionInfo(True, "Show labels for page tabs"), + "uiux_show_input_range_ticks": OptionInfo(True, "Show ticks for input range slider", gr.Checkbox, {"visible": False}), + "uiux_no_headers_params": OptionInfo(False, "Hide params headers", gr.Checkbox, {"visible": False}), + "uiux_show_outline_params": OptionInfo(True, "Show parameter outline", gr.Checkbox, {"visible": False}), + + 'uiux_separator_mobile': OptionInfo("

Mobile

", "", gr.HTML), + "uiux_default_layout": OptionInfo("Auto", "Layout", gr.Radio, {"choices": ["Auto","Desktop", "Mobile"]}), + "uiux_mobile_scale": OptionInfo(0.7, "Mobile scale", gr.Slider, {"minimum": 0.5, "maximum": 1, "step": 0.05}), + "images_sep_log": OptionInfo("

Log Display

", "", gr.HTML), "logmonitor_show": OptionInfo(True, "Show log view"), "logmonitor_refresh_period": OptionInfo(5000, "Log view update period", gr.Slider, {"minimum": 0, "maximum": 30000, "step": 25}),