From b1c66f055a3c9f7597f9a54ab2203134938e63af Mon Sep 17 00:00:00 2001 From: sorlinv Date: Wed, 4 Mar 2026 09:52:17 +0100 Subject: [PATCH] better render queue multi --- .claude/settings.local.json | 3 +- src/main/QueueManager.js | 140 +++++++++++++++++++++++++-- src/renderer/scripts/ProgressBar.js | 2 +- src/renderer/scripts/RenderQueue.js | 141 ++++++++++++++++++++++++++-- 4 files changed, 268 insertions(+), 18 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9e60fe3..348ec5a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -30,7 +30,8 @@ "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f''Release creee: id={d.get\\(\"\"id\"\", \"\"ERREUR\"\"\\)} tag={d.get\\(\"\"tag_name\"\", \"\"?\"\"\\)}''\\); print\\(d.get\\(''message'',''''\\)\\) if ''message'' in d else None\")", "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f'' OK: {d.get\\(\"\"name\"\",\"\"?\"\"\\)} \\({d.get\\(\"\"size\"\",0\\)//1024//1024}MB\\)''\\)\")", "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f''OK: {d.get\\(\"\"name\"\",d.get\\(\"\"message\"\",\"\"?\"\"\\)\\)} size={d.get\\(\"\"size\"\",0\\)//1024//1024}MB''\\)\")", - "Bash(python3 -c \":*)" + "Bash(python3 -c \":*)", + "mcp__ide__getDiagnostics" ] } } diff --git a/src/main/QueueManager.js b/src/main/QueueManager.js index ad30904..8ca74b8 100644 --- a/src/main/QueueManager.js +++ b/src/main/QueueManager.js @@ -1,13 +1,13 @@ const BlenderProcess = require("./BlenderProcess.js"); const path = require("path"); const fs = require("fs"); +const os = require("os"); const { Notification } = require("electron"); const STR_STATUS_IDLE = "idle"; const STR_STATUS_RUNNING = "running"; const STR_STATUS_PAUSED = "paused"; -const STR_PLACEHOLDER_CONTENT = "RENDERING"; -const NB_PLACEHOLDER_MAX_SIZE = 64; +const NB_PLACEHOLDER_MAX_SIZE = 512; class QueueManager { constructor(obj_window) { @@ -19,6 +19,10 @@ class QueueManager { this.nb_last_render_ms = 0; this.str_last_image_path = null; this.obj_notification_config = { is_notify_each_image: false, is_notify_all_done: true }; + this.str_hostname = os.hostname(); + this.nb_total_render_ms = 0; + this.nb_completed_renders = 0; + this.map_remote_avgs = {}; } set_notification_config(obj_config) { @@ -38,6 +42,9 @@ class QueueManager { this.str_status = STR_STATUS_RUNNING; this.nb_last_render_ms = 0; this.str_last_image_path = null; + this.nb_total_render_ms = 0; + this.nb_completed_renders = 0; + this.map_remote_avgs = {}; this.str_overwrite_mode = obj_config.str_overwrite_mode || "overwrite"; this.list_collections = obj_config.list_collections || []; this.obj_render_settings = obj_config.obj_render_settings || null; @@ -194,6 +201,8 @@ class QueueManager { return; } + this._check_remote_completions(); + // Batch skip : boucle iterative pour eviter un stack overflow recursif let nb_skip_count = 0; while (this.nb_current_index < this.list_queue.length && this.str_overwrite_mode === "skip") { @@ -215,7 +224,18 @@ class QueueManager { if (nb_size === 0) { break; } - obj_check.str_status = "skipped"; + + if (nb_size > NB_PLACEHOLDER_MAX_SIZE) { + obj_check.str_status = "skipped"; + try { + obj_check.str_done_date = fs.statSync(obj_check.str_expected_file).mtime.toISOString(); + } catch (obj_date_err) { + // ignore + } + } else { + obj_check.str_status = "rendering_remote"; + this._read_placeholder(obj_check); + } this.nb_current_index++; nb_skip_count++; } @@ -242,13 +262,26 @@ class QueueManager { try { if (fs.existsSync(obj_item.str_expected_file)) { let nb_recheck_size = fs.statSync(obj_item.str_expected_file).size; - if (nb_recheck_size > 0) { + if (nb_recheck_size > NB_PLACEHOLDER_MAX_SIZE) { obj_item.str_status = "skipped"; + try { + obj_item.str_done_date = fs.statSync(obj_item.str_expected_file).mtime.toISOString(); + } catch (obj_d_err) { + // ignore + } this._send_log("Skip : " + obj_item.str_camera_name + " F" + obj_item.nb_frame + " (existant)"); this.nb_current_index++; this._send_progress(); this._process_next(); return; + } else if (nb_recheck_size > 0) { + obj_item.str_status = "rendering_remote"; + this._read_placeholder(obj_item); + this._send_log("Skip : " + obj_item.str_camera_name + " F" + obj_item.nb_frame + " (en cours par " + (obj_item.str_remote_hostname || "?") + ")"); + this.nb_current_index++; + this._send_progress(); + this._process_next(); + return; } } } catch (obj_recheck_err) { @@ -264,7 +297,7 @@ class QueueManager { if (!fs.existsSync(str_dir)) { fs.mkdirSync(str_dir, { recursive: true }); } - fs.writeFileSync(obj_item.str_expected_file, STR_PLACEHOLDER_CONTENT); + fs.writeFileSync(obj_item.str_expected_file, this._get_placeholder_content()); } catch (obj_file_err) { this._send_log("ERREUR creation placeholder : " + obj_file_err.message); } @@ -301,6 +334,9 @@ class QueueManager { obj_item.str_status = "done"; this.nb_last_render_ms = Date.now() - nb_start; + this.nb_total_render_ms += this.nb_last_render_ms; + this.nb_completed_renders++; + obj_item.str_done_date = new Date().toISOString(); let str_image = obj_result.str_rendered_file || obj_item.str_expected_file; this.str_last_image_path = str_image; @@ -356,14 +392,54 @@ class QueueManager { let list_skipped = []; let list_stopped = []; let list_skipped_paths = []; + let list_rendering_remote = []; + let list_item_results = []; + + let nb_avg = this.nb_completed_renders > 0 + ? Math.round(this.nb_total_render_ms / this.nb_completed_renders) + : 0; + for (let nb_i = 0; nb_i < this.list_queue.length; nb_i++) { - if (this.list_queue[nb_i].str_status === "skipped") { + let obj_q = this.list_queue[nb_i]; + if (obj_q.str_status === "skipped") { list_skipped.push(nb_i); - list_skipped_paths.push({ nb_index: nb_i, str_path: this.list_queue[nb_i].str_expected_file }); - } else if (this.list_queue[nb_i].str_status === "stopped") { + list_skipped_paths.push({ nb_index: nb_i, str_path: obj_q.str_expected_file }); + list_item_results.push({ + nb_index: nb_i, + str_type: "done", + str_date: obj_q.str_done_date || null, + str_resolution: obj_q.nb_resolution_x + "x" + obj_q.nb_resolution_y, + }); + } else if (obj_q.str_status === "stopped") { list_stopped.push(nb_i); + } else if (obj_q.str_status === "rendering_remote") { + list_rendering_remote.push(nb_i); + list_item_results.push({ + nb_index: nb_i, + str_type: "rendering_remote", + str_remote_hostname: obj_q.str_remote_hostname || "?", + nb_remote_avg_ms: obj_q.nb_remote_avg_ms || 0, + }); + } else if (obj_q.str_status === "done") { + list_item_results.push({ + nb_index: nb_i, + str_type: "done", + str_date: obj_q.str_done_date || null, + str_resolution: obj_q.nb_resolution_x + "x" + obj_q.nb_resolution_y, + }); } } + + let list_machine_avgs = []; + if (nb_avg > 0) { + list_machine_avgs.push({ str_hostname: this.str_hostname, nb_avg_ms: nb_avg }); + } + for (let str_h of Object.keys(this.map_remote_avgs)) { + if (this.map_remote_avgs[str_h] > 0) { + list_machine_avgs.push({ str_hostname: str_h, nb_avg_ms: this.map_remote_avgs[str_h] }); + } + } + this._send_event("render-progress", { nb_current: this.nb_current_index, nb_total: this.list_queue.length, @@ -375,9 +451,57 @@ class QueueManager { list_skipped: list_skipped, list_stopped: list_stopped, list_skipped_paths: list_skipped_paths, + list_rendering_remote: list_rendering_remote, + list_item_results: list_item_results, + str_hostname: this.str_hostname, + nb_avg_render_ms: nb_avg, + list_machine_avgs: list_machine_avgs, }); } + _get_placeholder_content() { + let nb_avg = this.nb_completed_renders > 0 + ? Math.round(this.nb_total_render_ms / this.nb_completed_renders) + : 0; + return JSON.stringify({ str_hostname: this.str_hostname, nb_avg_ms: nb_avg }); + } + + _read_placeholder(obj_item) { + try { + let str_content = fs.readFileSync(obj_item.str_expected_file, "utf-8"); + let obj_data = JSON.parse(str_content); + obj_item.str_remote_hostname = obj_data.str_hostname || "?"; + obj_item.nb_remote_avg_ms = obj_data.nb_avg_ms || 0; + if (obj_data.str_hostname && obj_data.nb_avg_ms > 0) { + this.map_remote_avgs[obj_data.str_hostname] = obj_data.nb_avg_ms; + } + } catch (obj_parse_err) { + obj_item.str_remote_hostname = "?"; + obj_item.nb_remote_avg_ms = 0; + } + } + + _check_remote_completions() { + for (let obj_q of this.list_queue) { + if (obj_q.str_status !== "rendering_remote") { + continue; + } + try { + if (fs.existsSync(obj_q.str_expected_file)) { + let nb_size = fs.statSync(obj_q.str_expected_file).size; + if (nb_size > NB_PLACEHOLDER_MAX_SIZE) { + obj_q.str_status = "skipped"; + obj_q.str_done_date = fs.statSync(obj_q.str_expected_file).mtime.toISOString(); + } + } else { + obj_q.str_status = "pending"; + } + } catch (obj_check_err) { + // ignore + } + } + } + _send_log(str_message) { this._send_event("log", str_message); } diff --git a/src/renderer/scripts/ProgressBar.js b/src/renderer/scripts/ProgressBar.js index f0b31f6..3a39b8b 100644 --- a/src/renderer/scripts/ProgressBar.js +++ b/src/renderer/scripts/ProgressBar.js @@ -40,7 +40,7 @@ const ProgressBar = { let obj_frame_label = document.getElementById("label_current_frame"); obj_frame_label.textContent = nb_frame !== null && nb_frame !== undefined ? String(nb_frame) : "-"; - RenderQueue.update_progress(nb_current, obj_data.nb_last_render_ms || 0, obj_data.str_last_image_path || null, obj_data.list_skipped || [], obj_data.list_stopped || [], obj_data.list_skipped_paths || []); + RenderQueue.update_progress(obj_data); }, reset: () => { diff --git a/src/renderer/scripts/RenderQueue.js b/src/renderer/scripts/RenderQueue.js index b3307b0..9508bbf 100644 --- a/src/renderer/scripts/RenderQueue.js +++ b/src/renderer/scripts/RenderQueue.js @@ -3,6 +3,9 @@ const RenderQueue = { nb_total_render_ms: 0, nb_completed_renders: 0, nb_last_current: 0, + list_machine_avgs: [], + nb_local_avg_ms: 0, + str_hostname: "", init: () => { // Initialized on demand @@ -13,6 +16,9 @@ const RenderQueue = { RenderQueue.nb_total_render_ms = 0; RenderQueue.nb_completed_renders = 0; RenderQueue.nb_last_current = 0; + RenderQueue.list_machine_avgs = []; + RenderQueue.nb_local_avg_ms = 0; + RenderQueue.str_hostname = ""; let list_enabled = []; for (let obj_cam of list_cameras) { @@ -32,6 +38,7 @@ const RenderQueue = { str_image_path: null, obj_dom_el: null, obj_dom_icon: null, + obj_dom_result: null, str_dom_status: null, is_click_bound: false, }); @@ -60,6 +67,7 @@ const RenderQueue = { str_image_path: null, obj_dom_el: null, obj_dom_icon: null, + obj_dom_result: null, str_dom_status: null, is_click_bound: false, }); @@ -101,12 +109,20 @@ const RenderQueue = { obj_frame.classList.add("text-light-emphasis"); obj_frame.textContent = "F" + obj_item.nb_frame; + let obj_result = document.createElement("small"); + obj_result.classList.add("text-light-emphasis", "queue-item-result"); + obj_result.style.minWidth = "140px"; + obj_result.style.textAlign = "right"; + obj_result.style.fontSize = "0.75em"; + obj_el.appendChild(obj_icon); obj_el.appendChild(obj_name); obj_el.appendChild(obj_frame); + obj_el.appendChild(obj_result); obj_item.obj_dom_el = obj_el; obj_item.obj_dom_icon = obj_icon; + obj_item.obj_dom_result = obj_result; obj_item.str_dom_status = "pending"; obj_fragment.appendChild(obj_el); @@ -115,7 +131,21 @@ const RenderQueue = { obj_container.appendChild(obj_fragment); }, - update_progress: (nb_current, nb_last_render_ms, str_last_image_path, list_skipped, list_stopped, list_skipped_paths) => { + update_progress: (obj_data) => { + let nb_current = obj_data.nb_current || 0; + let nb_last_render_ms = obj_data.nb_last_render_ms || 0; + let str_last_image_path = obj_data.str_last_image_path || null; + let list_skipped = obj_data.list_skipped || []; + let list_stopped = obj_data.list_stopped || []; + let list_skipped_paths = obj_data.list_skipped_paths || []; + let list_rendering_remote = obj_data.list_rendering_remote || []; + let list_item_results = obj_data.list_item_results || []; + let list_machine_avgs = obj_data.list_machine_avgs || []; + + RenderQueue.str_hostname = obj_data.str_hostname || ""; + RenderQueue.nb_local_avg_ms = obj_data.nb_avg_render_ms || 0; + RenderQueue.list_machine_avgs = list_machine_avgs; + if (nb_current > RenderQueue.nb_last_current && nb_last_render_ms > 0) { RenderQueue.nb_total_render_ms += nb_last_render_ms; RenderQueue.nb_completed_renders++; @@ -134,9 +164,17 @@ const RenderQueue = { } } + // Build result map for quick lookup + let map_results = {}; + for (let obj_r of list_item_results) { + map_results[obj_r.nb_index] = obj_r; + } + for (let nb_i = 0; nb_i < RenderQueue.list_items.length; nb_i++) { if (list_stopped && list_stopped.indexOf(nb_i) !== -1) { RenderQueue.list_items[nb_i].str_status = "stopped"; + } else if (list_rendering_remote && list_rendering_remote.indexOf(nb_i) !== -1) { + RenderQueue.list_items[nb_i].str_status = "rendering_remote"; } else if (list_skipped && list_skipped.indexOf(nb_i) !== -1) { RenderQueue.list_items[nb_i].str_status = "skipped"; } else if (nb_i < nb_current) { @@ -146,6 +184,11 @@ const RenderQueue = { } else { RenderQueue.list_items[nb_i].str_status = "pending"; } + + // Store result info on item + if (map_results[nb_i]) { + RenderQueue.list_items[nb_i].obj_result = map_results[nb_i]; + } } RenderQueue._update_time_display(); @@ -163,7 +206,9 @@ const RenderQueue = { let is_needs_click = (obj_item.str_status === "done" || obj_item.str_status === "skipped") && obj_item.str_image_path && !obj_item.is_click_bound; - if (obj_item.str_status === obj_item.str_dom_status && !is_needs_click) { + let is_status_changed = obj_item.str_status !== obj_item.str_dom_status; + + if (!is_status_changed && !is_needs_click) { if (obj_item.str_status === "rendering") { obj_rendering_el = obj_item.obj_dom_el; } @@ -189,10 +234,18 @@ const RenderQueue = { } else if (obj_item.str_status === "stopped") { str_icon = "mdi-stop-circle"; str_color = "text-warning"; + } else if (obj_item.str_status === "rendering_remote") { + str_icon = "mdi-desktop-classic"; + str_color = "text-warning"; } obj_item.obj_dom_icon.className = "mdi " + str_icon + " " + str_color; + // Update result column + if (obj_item.obj_dom_result) { + RenderQueue._update_result_cell(obj_item); + } + if (is_needs_click) { obj_item.obj_dom_el.classList.add("queue-item-clickable"); let str_path = obj_item.str_image_path; @@ -210,6 +263,45 @@ const RenderQueue = { } }, + _update_result_cell: (obj_item) => { + let obj_r = obj_item.obj_result; + if (!obj_r) { + obj_item.obj_dom_result.textContent = ""; + return; + } + + if (obj_r.str_type === "done") { + let str_date = ""; + if (obj_r.str_date) { + let obj_d = new Date(obj_r.str_date); + let str_day = String(obj_d.getDate()).padStart(2, "0"); + let str_month = String(obj_d.getMonth() + 1).padStart(2, "0"); + let str_hours = String(obj_d.getHours()).padStart(2, "0"); + let str_minutes = String(obj_d.getMinutes()).padStart(2, "0"); + str_date = str_day + "/" + str_month + " " + str_hours + ":" + str_minutes; + } + let str_res = obj_r.str_resolution || ""; + obj_item.obj_dom_result.textContent = str_date + (str_date && str_res ? " | " : "") + str_res; + obj_item.obj_dom_result.className = "text-success queue-item-result"; + obj_item.obj_dom_result.style.minWidth = "140px"; + obj_item.obj_dom_result.style.textAlign = "right"; + obj_item.obj_dom_result.style.fontSize = "0.75em"; + } else if (obj_r.str_type === "rendering_remote") { + let str_host = obj_r.str_remote_hostname || "?"; + let str_avg = ""; + if (obj_r.nb_remote_avg_ms > 0) { + str_avg = " ~" + RenderQueue._format_duration(obj_r.nb_remote_avg_ms) + "/i"; + } + obj_item.obj_dom_result.textContent = str_host + str_avg; + obj_item.obj_dom_result.className = "text-warning queue-item-result"; + obj_item.obj_dom_result.style.minWidth = "140px"; + obj_item.obj_dom_result.style.textAlign = "right"; + obj_item.obj_dom_result.style.fontSize = "0.75em"; + } else { + obj_item.obj_dom_result.textContent = ""; + } + }, + mark_existing: (list_existing) => { for (let obj_existing of list_existing) { if (obj_existing.nb_index < RenderQueue.list_items.length) { @@ -226,22 +318,55 @@ const RenderQueue = { return; } - if (RenderQueue.nb_completed_renders === 0) { + let nb_local_avg = RenderQueue.nb_local_avg_ms; + if (nb_local_avg === 0 && RenderQueue.nb_completed_renders > 0) { + nb_local_avg = Math.round(RenderQueue.nb_total_render_ms / RenderQueue.nb_completed_renders); + } + + if (nb_local_avg === 0) { obj_label.innerHTML = ""; return; } - let nb_avg_ms = RenderQueue.nb_total_render_ms / RenderQueue.nb_completed_renders; let nb_remaining_count = 0; for (let obj_item of RenderQueue.list_items) { - if (obj_item.str_status !== "done" && obj_item.str_status !== "skipped") { + if (obj_item.str_status !== "done" && obj_item.str_status !== "skipped" && obj_item.str_status !== "rendering_remote") { nb_remaining_count++; } } - let nb_remaining_ms = nb_avg_ms * nb_remaining_count; - obj_label.innerHTML = '' - + RenderQueue._format_duration(nb_remaining_ms); + // Part 1 : temps moyen machine actuelle /i + let str_local_avg = RenderQueue._format_duration(nb_local_avg) + "/i"; + + // Part 2 : temps restant si machine seule + let nb_remaining_solo_ms = nb_local_avg * nb_remaining_count; + let str_remaining_solo = RenderQueue._format_duration(nb_remaining_solo_ms); + + // Part 3 : temps restant multi-machines + let list_avgs = RenderQueue.list_machine_avgs; + let nb_machines_with_data = 0; + let nb_sum_avgs = 0; + for (let obj_m of list_avgs) { + if (obj_m.nb_avg_ms > 0) { + nb_sum_avgs += obj_m.nb_avg_ms; + nb_machines_with_data++; + } + } + + let str_remaining_multi = ""; + if (nb_machines_with_data > 1) { + let nb_avg_of_avgs = nb_sum_avgs / nb_machines_with_data; + let nb_remaining_multi_ms = (nb_avg_of_avgs * nb_remaining_count) / nb_machines_with_data; + str_remaining_multi = RenderQueue._format_duration(nb_remaining_multi_ms); + } + + let str_display = '' + + str_local_avg + " | " + str_remaining_solo; + if (str_remaining_multi) { + str_display += " | " + str_remaining_multi; + } + + obj_label.innerHTML = str_display; }, _format_duration: (nb_ms) => {