From 1c5b3a47e28c530810e0cd4ba1c797686ac3e7b4 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 12 Sep 2025 22:14:07 -0400 Subject: [PATCH] job history Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- javascript/history.js | 48 ++++++++++++ javascript/timesheet.css | 121 +++++++++++++++++++++++++++++ javascript/timesheet.js | 53 +++++++++++++ modules/api/models.py | 1 + modules/processing_diffusers.py | 4 +- modules/shared_state.py | 12 ++- modules/ui_history.py | 53 +------------ modules/ui_javascript.py | 14 +++- 9 files changed, 250 insertions(+), 58 deletions(-) create mode 100644 javascript/history.js create mode 100644 javascript/timesheet.css create mode 100644 javascript/timesheet.js diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index c72f08828..430c8140c 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit c72f08828e1cf10b53a8128e2e7c0b6de1f168d5 +Subproject commit 430c8140c0cdda5e82a80f44ee6961e0d803e516 diff --git a/javascript/history.js b/javascript/history.js new file mode 100644 index 000000000..805c8e4e4 --- /dev/null +++ b/javascript/history.js @@ -0,0 +1,48 @@ +function refreshHistory() { + log('refreshHistory'); + fetch(`${window.api}/history`, { priority: 'low' }).then((res) => { + const timeline = document.getElementById('history_timeline'); + const table = document.getElementById('history_table'); + res.json().then((data) => { + if (!data || !data.length) { + table.innerHTML = '

No history data available.

'; + return; + } + + // build table + let html = ''; + for (const entry of data) { + const ts = new Date(1000 * entry.timestamp).toLocaleString(); + const duration = entry.duration ? `${(entry.duration).toFixed(3)}` : ''; + const outputs = entry.outputs.join(', '); + html += ``; + } + html += '
TimeIDJobActionDurationOutputs
${ts}${entry.id}${entry.job}${entry.op}${duration}${outputs}
'; + table.innerHTML = html; + + // crop data to last processing session + let startIdx = -1; + for (let i = data.length - 1; i >= 0; --i) { + const e = data[i]; + if ((e.job === 'control' || e.job === 'text' || e.job === 'control' || e.job === 'image') && (e.op === 'begin')) { + startIdx = i; + break; + } + } + if (startIdx >= 0) data = data.slice(startIdx); + + // build timeline + const ts = []; + for (const entry of data) { + if (entry.op === 'begin') { + const start = entry.timestamp; + const end = data.find((e) => (e.id === entry.id && e.op === 'end')) || data[data.length - 1].timestamp; + ts.push({ start, end: end.timestamp, label: entry.job, type: '' }); + } + } + timeline.innerHTML = ''; + if (!ts.length) return; + new Timesheet(timeline, ts); // eslint-disable-line no-undef, no-new + }); + }); +} diff --git a/javascript/timesheet.css b/javascript/timesheet.css new file mode 100644 index 000000000..c84165ab1 --- /dev/null +++ b/javascript/timesheet.css @@ -0,0 +1,121 @@ +.timesheet { + width: 100%; + height: auto; + margin: 0 auto; +} +.timesheet { + background-color: var(--sd-main-background-color); + position: relative; +} +.timesheet.color-scheme-default .bubble-default { + background-color: RGBA(252, 70, 74, 1); +} +.timesheet.color-scheme-default .bubble-lorem { + background-color: RGBA(154, 202, 39, 1); +} +.timesheet.color-scheme-default .bubble-ipsum { + background-color: RGBA(60, 182, 227, 1); +} +.timesheet.color-scheme-default .bubble-dolor { + background-color: RGBA(244, 207, 48, 1); +} +.timesheet.color-scheme-default .bubble-sit { + background-color: RGBA(169, 105, 202, 1); +} +.timesheet.color-scheme-alternative .bubble-default { + background-color: #f3552e; +} +.timesheet.color-scheme-alternative .bubble-lorem { + background-color: #88c33a; +} +.timesheet.color-scheme-alternative .bubble-ipsum { + background-color: #436ae0; +} +.timesheet.color-scheme-alternative .bubble-dolor { + background-color: #f4d234; +} +.timesheet.color-scheme-alternative .bubble-sit { + background-color: #707d86; +} +.timesheet .scale { + height: 100%; + position: absolute; + top: 0; + left: 0; + float: left; +} +.timesheet .scale section { + float: left; + text-align: center; + color: rgba(250, 250, 250, 0.8); + font-size: 13px; + line-height: 24px; + font-weight: lighter; + border-left: 1px dashed rgba(250, 250, 250, 0.2); + height: 100%} +.timesheet .data { + margin: 10px 0 10px 0; + padding: 0; + text-align: left; + list-style-type: none; + color: rgba(250, 250, 250, 0.8); + font-size: 13px; + overflow: hidden; +} +.timesheet .data li { + margin: 0 0 3px 0; + line-height: 22px; + height: 21px; + display: block; + clear: both; + position: relative; + white-space: nowrap; +} +.timesheet .data li:hover .bubble { + opacity: 1; +} +.timesheet .data li .date { + color: #b5b5b5; + font-size: 14px; +} +.timesheet .data li .label { + font-weight: lighter; + font-size: 14px; + padding-left: 5px; + line-height: 21px; + color: #979796; + white-space: nowrap; +} +.timesheet .data li .bubble { + width: 24px; + height: 7px; + display: block; + float: left; + position: relative; + top: 7px; + border-radius: 4px; + margin: 0 10px 0 0; + opacity: 0.7; + background: var(--sd-main-accent-color); +} +#timesheet-alternative { + background-color: RGBA(247, 247, 247, 1); + border-radius: 5px; +} +#timesheet-alternative section { + color: RGBA(63, 68, 72, 1); + border-left: 1px dashed RGBA(63, 68, 72, 0.2); +} +#timesheet-alternative section:first-child { + border-left: 1px dashed transparent; +} +#timesheet-alternative .date { + display: none; +} +#timesheet-alternative .bubble { + margin-right: 7px; +} +#timesheet-alternative .label { + padding-left: 0px; + color: RGBA(48, 48, 48, 1); +} diff --git a/javascript/timesheet.js b/javascript/timesheet.js new file mode 100644 index 000000000..679a75944 --- /dev/null +++ b/javascript/timesheet.js @@ -0,0 +1,53 @@ +/* eslint max-classes-per-file: ["error", 2] */ + +class Bubble { + constructor(min, start, end, label, scale) { + this.min = min; + this.start = start; + this.end = end; + this.scale = scale; + this.offset = Math.round(this.scale * (this.start - this.min)); + this.width = Math.round(this.scale * (this.end - this.start)); + this.label = label; + this.duration = Math.round(1000 * (this.end - this.start)) / 1000; + this.title = `Job: ${this.label}\nDuration: ${this.duration}s\nStart: ${new Date(1000 * this.start).toLocaleString()}\nEnd: ${new Date(1000 * this.end).toLocaleString()}`; + } + + getDateLabel() { + return Math.round(1000 * (this.end - this.start)) / 1000; + } +} + +class Timesheet { + constructor(container, data) { + this.min = Math.floor(data[0].start); + this.max = Math.round(data[data.length - 1].end + 0.5); + this.data = data; + this.container = container; + const box = container.getBoundingClientRect(); + const width = box.width - 140; + this.scale = width / (this.max - this.min); + + // draw sections + let html = []; + for (let c = 0; c <= this.max - this.min; c++) html.push(`
`); + container.className = 'timesheet color-scheme-default'; + container.innerHTML = `
${html.join('')}
`; + + // insert data + html = []; + for (let n = 0, m = this.data.length; n < m; n++) { + const cur = this.data[n]; + const bubble = new Bubble(this.min, cur.start, cur.end, cur.label, this.scale); + const line = [ + ``, + `${bubble.duration} `, + `${bubble.label}`, + ].join(''); + html.push(`
  • ${line}
  • `); + } + this.container.innerHTML += ``; + } +} + +window.Timesheet = Timesheet; diff --git a/modules/api/models.py b/modules/api/models.py index 94dd9e3a5..61d3dff66 100644 --- a/modules/api/models.py +++ b/modules/api/models.py @@ -341,6 +341,7 @@ class ResHistory(BaseModel): job: str = Field(title="Job", description="Job name") op: str = Field(title="Operation", description="Job state") timestamp: Union[float, None] = Field(title="Timestamp", description="Job timestamp") + duration: Union[float, None] = Field(title="Duration", description="Job duration") outputs: List[str] = Field(title="Outputs", description="List of filenames") class ResStatus(BaseModel): diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index 3b9099621..579352c56 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -164,7 +164,7 @@ def process_base(p: processing.StableDiffusionProcessing): base_args['gate_step'] = p.gate_step output = shared.sd_model.tgate(**base_args) # pylint: disable=not-callable else: - taskid = shared.state.begin('Model') + taskid = shared.state.begin('Inference') output = shared.sd_model(**base_args) shared.state.end(taskid) if isinstance(output, dict): @@ -296,7 +296,7 @@ def process_hires(p: processing.StableDiffusionProcessing, output): try: if 'base' in p.skip: extra_networks.activate(p) - taskid = shared.state.begin('Model') + taskid = shared.state.begin('Inference') output = shared.sd_model(**hires_args) # pylint: disable=not-callable shared.state.end(taskid) if isinstance(output, dict): diff --git a/modules/shared_state.py b/modules/shared_state.py index 53e044727..32df815c2 100644 --- a/modules/shared_state.py +++ b/modules/shared_state.py @@ -45,6 +45,7 @@ class State: disable_preview = False preview_job = -1 time_start = None + duration = None need_restart = False server_start = time.time() oom = False @@ -148,7 +149,14 @@ class State: return None def history(self, op:str, task_id:str=None, results:list=[]): - job = { 'id': task_id or self.id, 'job': self.job.lower(), 'op': op.lower(), 'timestamp': self.time_start, 'outputs': results } + job = { + 'id': task_id or self.id, + 'job': self.job.lower(), + 'op': op.lower(), + 'timestamp': self.time_start, + 'duration': self.duration, + 'outputs': results, + } self.state_history.append(job) l = len(self.state_history) if l > 10000: @@ -179,6 +187,7 @@ class State: self.job_no = 0 self.frame_count = 0 self.preview_job = -1 + self.duration = None self.paused = False self.interrupted = False self.skipped = False @@ -223,6 +232,7 @@ class State: if prev_job is not None: self.id = prev_job['id'] self.job = prev_job['job'] + self.duration = round(time.time() - prev_job['timestamp'], 3) if prev_job['timestamp'] is not None else None self.time_start = time.time() self.history('end', task_id or self.id) self.clear() diff --git a/modules/ui_history.py b/modules/ui_history.py index 9d642ec40..8cfc43bdb 100644 --- a/modules/ui_history.py +++ b/modules/ui_history.py @@ -1,58 +1,11 @@ -import time import gradio as gr -from modules import shared - - -def refresh(): - def ts(t): - try: - return time.strftime('%Y-%m-%d %H:%M:%S.%f', time.localtime(t)) - except Exception: - return '' - - items = [] - for item in shared.state.state_history: - items.append([ - item['id'], - item['job'], - item['op'], - ts(item['timestamp']), - len(item['outputs']), - ]) - shared.log.info(f"History: records={len(items)}") - if len(items) == 0: - items = None - return gr.Dataframe.update(value=items) - - -def select(evt: gr.SelectData, _data): - if evt.index is None or len(evt.index) == 0: - return [] - row = evt.index[0] - item = shared.state.state_history[row] if row < len(shared.state.state_history) else {} - shared.log.debug(f"History: select={row}:{len(shared.state.state_history)} {item}") - files = item.get('outputs', []) - return gr.Files.update(value=files) def create_ui(): with gr.Row(): btn_refresh = gr.Button("Refresh", elem_id='btn_history_refresh') with gr.Row(): - history_table = gr.DataFrame( - value=None, - headers=['ID', 'Job', 'Op', 'Timestamp', 'Outputs'], - label='History data', - show_label=True, - interactive=False, - wrap=True, - elem_id='history_table', - ) + _history_table = gr.HTML('', elem_id='history_table') with gr.Row(): - history_files = gr.Files( - label="Task files", - interactive=False, - elem_id='history_files', - ) - btn_refresh.click(fn=refresh, inputs=[], outputs=[history_table]) - history_table.select(fn=select, inputs=[history_table], outputs=[history_files]) + _history_timeline = gr.HTML('', elem_id='history_timeline') + btn_refresh.click(_js='refreshHistory', fn=None, inputs=[], outputs=[], show_progress=False) diff --git a/modules/ui_javascript.py b/modules/ui_javascript.py index 6668d33c5..c17e1ad14 100644 --- a/modules/ui_javascript.py +++ b/modules/ui_javascript.py @@ -52,13 +52,16 @@ def html_login(): return js -def html_css(css: str): +def html_css(css: list[str]): def stylesheet(fn): return f'' head = '' if css is not None: - head += stylesheet(os.path.join(script_path, 'javascript', css)) + for cssfile in css: + f = os.path.join(script_path, 'javascript', cssfile) + if os.path.isfile(f): + head += stylesheet(f) for cssfile in modules.scripts_manager.list_files_with_name("style.css"): if not os.path.isfile(cssfile): continue @@ -86,12 +89,15 @@ def html_css(css: str): def reload_javascript(): - base_css = theme.reload_gradio_theme() title = 'SD.Next' manifest = f'' login = html_login() js = html_head() - css = html_css(base_css) + + css_base = theme.reload_gradio_theme() + css_timesheet = "timesheet.css" + + css = html_css([css_base, css_timesheet]) body = html_body() def template_response(*args, **kwargs):