feat: add new dependency viewer, reused previous cache for faster deploy

This commit is contained in:
bennykok 2024-02-17 14:45:48 +08:00
parent 126d8e77fb
commit f3370c1bb1
3 changed files with 138 additions and 26 deletions

View File

@ -24,6 +24,8 @@ from urllib.parse import quote
import threading import threading
import hashlib import hashlib
import aiohttp import aiohttp
import aiofiles
import concurrent.futures
api = None api = None
api_task = None api_task = None
@ -163,15 +165,74 @@ def get_comfyui_path_from_file_path(file_path):
return file_path return file_path
# Form ComfyUI Manager # Form ComfyUI Manager
def compute_sha256_checksum(filepath): async def compute_sha256_checksum(filepath):
print("computing sha256 checksum")
chunk_size = 1024 * 256 # Example: 256KB
filepath = get_comfyui_path_from_file_path(filepath) filepath = get_comfyui_path_from_file_path(filepath)
"""Compute the SHA256 checksum of a file, in chunks""" """Compute the SHA256 checksum of a file, in chunks, asynchronously"""
sha256 = hashlib.sha256() sha256 = hashlib.sha256()
with open(filepath, 'rb') as f: async with aiofiles.open(filepath, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''): while True:
chunk = await f.read(chunk_size)
if not chunk:
break
sha256.update(chunk) sha256.update(chunk)
return sha256.hexdigest() return sha256.hexdigest()
# def hash_chunk(start_end, filepath):
# """Hash a specific chunk of the file."""
# start, end = start_end
# sha256 = hashlib.sha256()
# with open(filepath, 'rb') as f:
# f.seek(start)
# chunk = f.read(end - start)
# sha256.update(chunk)
# return sha256.digest() # Return the digest of the chunk
# async def compute_sha256_checksum(filepath):
# file_size = os.path.getsize(filepath)
# parts = 1 # Or any other division based on file size or desired concurrency
# part_size = file_size // parts
# start_end_ranges = [(i * part_size, min((i + 1) * part_size, file_size)) for i in range(parts)]
# print(start_end_ranges, file_size)
# loop = asyncio.get_running_loop()
# # Use ThreadPoolExecutor to process chunks in parallel
# with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
# futures = [loop.run_in_executor(executor, hash_chunk, start_end, filepath) for start_end in start_end_ranges]
# chunk_hashes = await asyncio.gather(*futures)
# # Combine the hashes sequentially
# final_sha256 = hashlib.sha256()
# for chunk_hash in chunk_hashes:
# final_sha256.update(chunk_hash)
# return final_sha256.hexdigest()
# def hash_chunk(filepath):
# chunk_size = 1024 * 256 # 256KB per chunk
# sha256 = hashlib.sha256()
# with open(filepath, 'rb') as f:
# while True:
# chunk = f.read(chunk_size)
# if not chunk:
# break # End of file
# sha256.update(chunk)
# return sha256.hexdigest()
# async def compute_sha256_checksum(filepath):
# print("computing sha256 checksum")
# filepath = get_comfyui_path_from_file_path(filepath)
# loop = asyncio.get_running_loop()
# with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
# task = loop.run_in_executor(executor, hash_chunk, filepath)
# return await task
# This is start uploading the files to Comfy Deploy # This is start uploading the files to Comfy Deploy
@server.PromptServer.instance.routes.post('/comfyui-deploy/upload-file') @server.PromptServer.instance.routes.post('/comfyui-deploy/upload-file')
async def upload_file(request): async def upload_file(request):
@ -269,9 +330,13 @@ async def get_file_hash(request):
base = folder_paths.base_path base = folder_paths.base_path
file_path = os.path.join(base, file_path) file_path = os.path.join(base, file_path)
# print("file_path", file_path) # print("file_path", file_path)
file_hash = compute_sha256_checksum( start_time = time.time() # Capture the start time
file_hash = await compute_sha256_checksum(
file_path file_path
) )
end_time = time.time() # Capture the end time after the code execution
elapsed_time = end_time - start_time # Calculate the elapsed time
print(f"Execution time: {elapsed_time} seconds")
return web.json_response({ return web.json_response({
"file_hash": file_hash "file_hash": file_hash
}) })

1
requirement.txt Normal file
View File

@ -0,0 +1 @@
aiofiles

View File

@ -3,6 +3,8 @@ import { api } from "./api.js";
import { ComfyWidgets, LGraphNode } from "./widgets.js"; import { ComfyWidgets, LGraphNode } from "./widgets.js";
import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.19"; import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.19";
const loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`;
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/ /** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
/** @type {ComfyExtension} */ /** @type {ComfyExtension} */
const ext = { const ext = {
@ -284,21 +286,37 @@ function addButton() {
} }
const ok = await confirmDialog.confirm( const ok = await confirmDialog.confirm(
"Confirm deployment -> " + `Confirm deployment`,
displayName + `
"<br><br><small><div style='font-weight: normal;'>" + <div>
endpoint +
"<div><br>", A new version will be deployed, do you confirm?
`A new version will be deployed, are you confirm? <br><br><input id="include-deps" type="checkbox" checked>Include dependence</input>`, <br><br>
<button style="font-size: 18px;">${displayName}</button>
<br>
<button style="font-size: 18px;">${endpoint}</button>
<br><br>
<label>
<input id="include-deps" type="checkbox" checked>Include dependency</input>
</label>
<br>
<label>
<input id="reuse-hash" type="checkbox" checked>Reuse hash from last version</input>
</label>
</div>
`,
); );
if (!ok) return; if (!ok) return;
const includeDeps = document.getElementById("include-deps").checked; const includeDeps = document.getElementById("include-deps").checked;
const reuseHash = document.getElementById("reuse-hash").checked;
if (endpoint.endsWith("/")) { if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1); endpoint = endpoint.slice(0, -1);
} }
loadingDialog.showLoading("Generating snapshot", "Please wait..."); loadingDialog.showLoading("Generating snapshot");
const snapshot = await fetch("/snapshot/get_current").then((x) => x.json()); const snapshot = await fetch("/snapshot/get_current").then((x) => x.json());
// console.log(snapshot); // console.log(snapshot);
@ -343,7 +361,7 @@ function addButton() {
let deps = undefined; let deps = undefined;
if (includeDeps) { if (includeDeps) {
loadingDialog.showLoading("Fetching existing version", "Please wait..."); loadingDialog.showLoading("Fetching existing version");
const existing_workflow = await fetch( const existing_workflow = await fetch(
endpoint + "/api/workflow/" + workflow_id, endpoint + "/api/workflow/" + workflow_id,
@ -362,14 +380,35 @@ function addButton() {
loadingDialog.close(); loadingDialog.close();
loadingDialog.showLoading( loadingDialog.showLoading("Generating dependency graph");
"Generating dependency graph",
"Please wait...",
);
deps = await generateDependencyGraph({ deps = await generateDependencyGraph({
workflow_api: prompt.output, workflow_api: prompt.output,
snapshot: snapshot, snapshot: snapshot,
computeFileHash: async (file) => { computeFileHash: async (file) => {
console.log(existing_workflow?.dependencies?.models);
// Match previous hash for models
if (reuseHash && existing_workflow?.dependencies?.models) {
const previousModelHash = Object.entries(
existing_workflow?.dependencies?.models,
).flatMap(([key, value]) => {
return Object.values(value).map((x) => ({
...x,
name: "models/" + key + "/" + x.name,
}));
});
console.log(previousModelHash);
const match = previousModelHash.find((x) => {
console.log(file, x.name);
return file == x.name;
});
console.log(match);
if (match && match.hash) {
console.log("cached hash used");
return match.hash;
}
}
console.log(file); console.log(file);
loadingDialog.showLoading("Generating hash", file); loadingDialog.showLoading("Generating hash", file);
const hash = await fetch( const hash = await fetch(
@ -416,7 +455,14 @@ function addButton() {
const depsOk = await confirmDialog.confirm( const depsOk = await confirmDialog.confirm(
"Check dependencies", "Check dependencies",
// JSON.stringify(deps, null, 2), // JSON.stringify(deps, null, 2),
createDynamicUIHtml(deps), `
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">${loadingIcon}</div>
<iframe
style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;"
src="${endpoint}/dependency-graph?deps=${encodeURIComponent(
JSON.stringify(deps),
)}" />`,
// createDynamicUIHtml(deps),
); );
if (!depsOk) return; if (!depsOk) return;
@ -570,12 +616,10 @@ export class InfoDialog extends ComfyDialog {
`); `);
} }
loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`;
showLoading(title, message) { showLoading(title, message) {
this.show(` this.show(`
<div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset"> <div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
<h3 style="margin: 0px; display: flex; align-items: center; justify-content: center;">${title} ${this.loadingIcon}</h3> <h3 style="margin: 0px; display: flex; align-items: center; justify-content: center;">${title} ${loadingIcon}</h3>
<label> <label>
${message} ${message}
</label> </label>
@ -620,7 +664,11 @@ export class LoadingDialog extends ComfyDialog {
<h3 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 12px;">${title} ${ <h3 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 12px;">${title} ${
this.loadingIcon this.loadingIcon
}</h3> }</h3>
${message ? `<label>${message}</label>` : ""} ${
message
? `<label style="max-width: 100%; white-space: pre-wrap; word-wrap: break-word;">${message}</label>`
: ""
}
</div> </div>
`); `);
} }
@ -739,11 +787,9 @@ export class ConfirmDialog extends InfoDialog {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.callback = resolve; this.callback = resolve;
this.show(` this.show(`
<div style="width: 100%; max-width: 600px; display: flex; gap: 18px; flex-direction: column; overflow: unset"> <div style="width: 100%; max-width: 600px; display: flex; gap: 18px; flex-direction: column; overflow: unset; position: relative;">
<h3 style="margin: 0px;">${title}</h3> <h3 style="margin: 0px;">${title}</h3>
<label> ${message}
${message}
</label>
</div> </div>
`); `);
}); });