Initial commit: ComfyUI node usage stats tracker
Tracks every node used in every prompt submission via SQLite, maps class_types to source packages, and exposes API endpoints and a frontend dialog for viewing per-package usage stats. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
68
__init__.py
Normal file
68
__init__.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
from server import PromptServer
|
||||||
|
|
||||||
|
from .mapper import NodePackageMapper
|
||||||
|
from .tracker import UsageTracker
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {}
|
||||||
|
WEB_DIRECTORY = "./js"
|
||||||
|
|
||||||
|
mapper = NodePackageMapper()
|
||||||
|
tracker = UsageTracker()
|
||||||
|
|
||||||
|
|
||||||
|
def on_prompt_handler(json_data):
|
||||||
|
"""Called on every prompt submission. Extracts class_types and records usage."""
|
||||||
|
try:
|
||||||
|
prompt = json_data.get("prompt", {})
|
||||||
|
class_types = set()
|
||||||
|
for node_id, node_data in prompt.items():
|
||||||
|
ct = node_data.get("class_type")
|
||||||
|
if ct:
|
||||||
|
class_types.add(ct)
|
||||||
|
if class_types:
|
||||||
|
tracker.record_usage(class_types, mapper)
|
||||||
|
except Exception:
|
||||||
|
logger.warning("nodes-stats: error recording usage", exc_info=True)
|
||||||
|
return json_data
|
||||||
|
|
||||||
|
|
||||||
|
PromptServer.instance.add_on_prompt_handler(on_prompt_handler)
|
||||||
|
|
||||||
|
|
||||||
|
routes = PromptServer.instance.routes
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get("/nodes-stats/packages")
|
||||||
|
async def get_package_stats(request):
|
||||||
|
try:
|
||||||
|
stats = tracker.get_package_stats(mapper)
|
||||||
|
return web.json_response(stats)
|
||||||
|
except Exception:
|
||||||
|
logger.error("nodes-stats: error getting package stats", exc_info=True)
|
||||||
|
return web.json_response({"error": "internal error"}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get("/nodes-stats/usage")
|
||||||
|
async def get_node_stats(request):
|
||||||
|
try:
|
||||||
|
stats = tracker.get_node_stats()
|
||||||
|
return web.json_response(stats)
|
||||||
|
except Exception:
|
||||||
|
logger.error("nodes-stats: error getting node stats", exc_info=True)
|
||||||
|
return web.json_response({"error": "internal error"}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.post("/nodes-stats/reset")
|
||||||
|
async def reset_stats(request):
|
||||||
|
try:
|
||||||
|
tracker.reset()
|
||||||
|
mapper.invalidate()
|
||||||
|
return web.json_response({"status": "ok"})
|
||||||
|
except Exception:
|
||||||
|
logger.error("nodes-stats: error resetting stats", exc_info=True)
|
||||||
|
return web.json_response({"error": "internal error"}, status=500)
|
||||||
178
js/nodes_stats.js
Normal file
178
js/nodes_stats.js
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { app } from "../../scripts/app.js";
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "comfyui.nodes_stats",
|
||||||
|
|
||||||
|
async setup() {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.textContent = "Node Stats";
|
||||||
|
btn.style.cssText =
|
||||||
|
"font-size:14px;padding:4px 12px;cursor:pointer;border:none;border-radius:4px;background:#333;color:#fff;";
|
||||||
|
btn.addEventListener("click", () => showStatsDialog());
|
||||||
|
|
||||||
|
// Insert into ComfyUI menu bar
|
||||||
|
const menu = document.querySelector(".comfy-menu .comfy-menu-btns") ||
|
||||||
|
document.querySelector(".comfy-menu");
|
||||||
|
if (menu) {
|
||||||
|
menu.appendChild(btn);
|
||||||
|
} else {
|
||||||
|
// Fallback: wait for menu to appear
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const m = document.querySelector(".comfy-menu .comfy-menu-btns") ||
|
||||||
|
document.querySelector(".comfy-menu");
|
||||||
|
if (m) {
|
||||||
|
m.appendChild(btn);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function showStatsDialog() {
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/nodes-stats/packages");
|
||||||
|
if (!resp.ok) {
|
||||||
|
alert("Failed to load node stats: HTTP " + resp.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data = await resp.json();
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
alert("Failed to load node stats: unexpected response format");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert("Failed to load node stats: " + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove existing dialog if any
|
||||||
|
const existing = document.getElementById("nodes-stats-dialog");
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
const overlay = document.createElement("div");
|
||||||
|
overlay.id = "nodes-stats-dialog";
|
||||||
|
overlay.style.cssText =
|
||||||
|
"position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:10000;display:flex;align-items:center;justify-content:center;";
|
||||||
|
overlay.addEventListener("click", (e) => {
|
||||||
|
if (e.target === overlay) overlay.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialog = document.createElement("div");
|
||||||
|
dialog.style.cssText =
|
||||||
|
"background:#1e1e1e;color:#ddd;border-radius:8px;padding:24px;max-width:800px;width:90%;max-height:85vh;overflow-y:auto;font-family:monospace;font-size:13px;";
|
||||||
|
|
||||||
|
const neverUsed = data.filter(
|
||||||
|
(p) => p.never_used && p.package !== "__builtin__"
|
||||||
|
);
|
||||||
|
const used = data.filter(
|
||||||
|
(p) => !p.never_used && p.package !== "__builtin__"
|
||||||
|
);
|
||||||
|
|
||||||
|
let html = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
||||||
|
<h2 style="margin:0;color:#fff;font-size:18px;">Node Package Stats</h2>
|
||||||
|
<button id="nodes-stats-close" style="background:none;border:none;color:#888;font-size:20px;cursor:pointer;">×</button>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
html += `<div style="display:flex;gap:16px;margin-bottom:20px;">
|
||||||
|
<div style="background:#3a1a1a;padding:8px 16px;border-radius:4px;border-left:3px solid #e44;">
|
||||||
|
<span style="font-size:22px;font-weight:bold;color:#e44;">${neverUsed.length}</span>
|
||||||
|
<span style="color:#c99;margin-left:6px;">never used</span>
|
||||||
|
</div>
|
||||||
|
<div style="background:#1a2a1a;padding:8px 16px;border-radius:4px;border-left:3px solid #4a4;">
|
||||||
|
<span style="font-size:22px;font-weight:bold;color:#4a4;">${used.length}</span>
|
||||||
|
<span style="color:#9c9;margin-left:6px;">used</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
if (neverUsed.length > 0) {
|
||||||
|
html += `<h3 style="color:#e44;margin:12px 0 8px;font-size:14px;">Never Used — Safe to Remove</h3>`;
|
||||||
|
html += buildTable(neverUsed, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (used.length > 0) {
|
||||||
|
html += `<h3 style="color:#4a4;margin:16px 0 8px;font-size:14px;">Used Packages</h3>`;
|
||||||
|
html += buildTable(used, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.innerHTML = html;
|
||||||
|
overlay.appendChild(dialog);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById("nodes-stats-close")
|
||||||
|
.addEventListener("click", () => overlay.remove());
|
||||||
|
|
||||||
|
// Toggle expandable rows
|
||||||
|
dialog.querySelectorAll(".pkg-row").forEach((row) => {
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
const detail = row.nextElementSibling;
|
||||||
|
if (detail && detail.classList.contains("pkg-detail")) {
|
||||||
|
detail.style.display =
|
||||||
|
detail.style.display === "none" ? "table-row" : "none";
|
||||||
|
const arrow = row.querySelector(".arrow");
|
||||||
|
if (arrow)
|
||||||
|
arrow.textContent = detail.style.display === "none" ? "▶" : "▼";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTable(packages, isNeverUsed) {
|
||||||
|
const bgColor = isNeverUsed ? "#2a1515" : "#151a15";
|
||||||
|
const hoverColor = isNeverUsed ? "#3a2020" : "#202a20";
|
||||||
|
|
||||||
|
let html = `<table style="width:100%;border-collapse:collapse;margin-bottom:12px;">
|
||||||
|
<thead><tr style="color:#888;text-align:left;border-bottom:1px solid #333;">
|
||||||
|
<th style="padding:6px 8px;"></th>
|
||||||
|
<th style="padding:6px 8px;">Package</th>
|
||||||
|
<th style="padding:6px 8px;text-align:right;">Nodes</th>
|
||||||
|
<th style="padding:6px 8px;text-align:right;">Used</th>
|
||||||
|
<th style="padding:6px 8px;text-align:right;">Executions</th>
|
||||||
|
<th style="padding:6px 8px;">Last Used</th>
|
||||||
|
</tr></thead><tbody>`;
|
||||||
|
|
||||||
|
for (const pkg of packages) {
|
||||||
|
const hasNodes = pkg.nodes && pkg.nodes.length > 0;
|
||||||
|
const lastSeen = pkg.last_seen
|
||||||
|
? new Date(pkg.last_seen).toLocaleDateString()
|
||||||
|
: "—";
|
||||||
|
|
||||||
|
html += `<tr class="pkg-row" style="cursor:${hasNodes ? "pointer" : "default"};background:${bgColor};border-bottom:1px solid #222;"
|
||||||
|
onmouseover="this.style.background='${hoverColor}'" onmouseout="this.style.background='${bgColor}'">
|
||||||
|
<td style="padding:6px 8px;width:20px;"><span class="arrow" style="color:#666;">${hasNodes ? "▶" : " "}</span></td>
|
||||||
|
<td style="padding:6px 8px;color:#fff;">${escapeHtml(pkg.package)}</td>
|
||||||
|
<td style="padding:6px 8px;text-align:right;">${pkg.total_nodes}</td>
|
||||||
|
<td style="padding:6px 8px;text-align:right;">${pkg.used_nodes}/${pkg.total_nodes}</td>
|
||||||
|
<td style="padding:6px 8px;text-align:right;">${pkg.total_executions}</td>
|
||||||
|
<td style="padding:6px 8px;color:#888;">${lastSeen}</td>
|
||||||
|
</tr>`;
|
||||||
|
|
||||||
|
if (hasNodes) {
|
||||||
|
html += `<tr class="pkg-detail" style="display:none;"><td colspan="6" style="padding:0 0 0 32px;">
|
||||||
|
<table style="width:100%;border-collapse:collapse;">`;
|
||||||
|
for (const node of pkg.nodes) {
|
||||||
|
const nLastSeen = node.last_seen
|
||||||
|
? new Date(node.last_seen).toLocaleDateString()
|
||||||
|
: "—";
|
||||||
|
html += `<tr style="border-bottom:1px solid #1a1a1a;color:#aaa;">
|
||||||
|
<td style="padding:3px 8px;">${escapeHtml(node.class_type)}</td>
|
||||||
|
<td style="padding:3px 8px;text-align:right;">${node.count}</td>
|
||||||
|
<td style="padding:3px 8px;color:#666;">${nLastSeen}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
html += `</table></td></tr>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `</tbody></table>`;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
68
mapper.py
Normal file
68
mapper.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NodePackageMapper:
|
||||||
|
"""Maps node class_type names to their source package."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._map = None
|
||||||
|
|
||||||
|
def _build_map(self):
|
||||||
|
import nodes
|
||||||
|
|
||||||
|
self._map = {}
|
||||||
|
for class_type, node_cls in nodes.NODE_CLASS_MAPPINGS.items():
|
||||||
|
module = getattr(node_cls, "RELATIVE_PYTHON_MODULE", None)
|
||||||
|
if module:
|
||||||
|
# "custom_nodes.PackageName" -> "PackageName"
|
||||||
|
# "comfy_extras.nodes_xyz" -> "__builtin__"
|
||||||
|
# "comfy_api_nodes.xyz" -> "__builtin__"
|
||||||
|
parts = module.split(".", 1)
|
||||||
|
if parts[0] == "custom_nodes" and len(parts) > 1:
|
||||||
|
self._map[class_type] = parts[1]
|
||||||
|
else:
|
||||||
|
self._map[class_type] = "__builtin__"
|
||||||
|
else:
|
||||||
|
self._map[class_type] = "__builtin__"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mapping(self):
|
||||||
|
if self._map is None:
|
||||||
|
self._build_map()
|
||||||
|
return self._map
|
||||||
|
|
||||||
|
def get_package(self, class_type):
|
||||||
|
return self.mapping.get(class_type, "__unknown__")
|
||||||
|
|
||||||
|
def get_all_packages(self):
|
||||||
|
"""Return set of all known package names, including zero-node packages."""
|
||||||
|
packages = set(self.mapping.values())
|
||||||
|
|
||||||
|
try:
|
||||||
|
import nodes
|
||||||
|
import folder_paths
|
||||||
|
|
||||||
|
# Get all custom_nodes directories to filter LOADED_MODULE_DIRS
|
||||||
|
custom_node_dirs = set()
|
||||||
|
for d in folder_paths.get_folder_paths("custom_nodes"):
|
||||||
|
custom_node_dirs.add(os.path.normpath(d))
|
||||||
|
|
||||||
|
# LOADED_MODULE_DIRS contains ALL modules (custom nodes, comfy_extras,
|
||||||
|
# comfy_api_nodes). We only want custom node packages, identified by
|
||||||
|
# their directory being directly inside a custom_nodes directory.
|
||||||
|
for module_name, module_dir in nodes.LOADED_MODULE_DIRS.items():
|
||||||
|
parent_dir = os.path.normpath(os.path.dirname(module_dir))
|
||||||
|
if parent_dir in custom_node_dirs:
|
||||||
|
packages.add(os.path.basename(module_dir))
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Could not read LOADED_MODULE_DIRS", exc_info=True)
|
||||||
|
|
||||||
|
packages.discard("__builtin__")
|
||||||
|
return packages
|
||||||
|
|
||||||
|
def invalidate(self):
|
||||||
|
"""Force rebuild on next access (e.g. after node reload)."""
|
||||||
|
self._map = None
|
||||||
13
pyproject.toml
Normal file
13
pyproject.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[project]
|
||||||
|
name = "comfyui-nodes-stats"
|
||||||
|
description = "Track usage statistics for all ComfyUI nodes and packages"
|
||||||
|
version = "1.0.0"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://github.com/placeholder/comfyui-nodes-stats"
|
||||||
|
|
||||||
|
[tool.comfy]
|
||||||
|
PublisherId = ""
|
||||||
|
DisplayName = "Node Usage Stats"
|
||||||
|
Icon = ""
|
||||||
152
tracker.py
Normal file
152
tracker.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import threading
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DB_PATH = os.path.join(os.path.dirname(__file__), "usage_stats.db")
|
||||||
|
|
||||||
|
SCHEMA = """
|
||||||
|
CREATE TABLE IF NOT EXISTS node_usage (
|
||||||
|
class_type TEXT PRIMARY KEY,
|
||||||
|
package TEXT NOT NULL,
|
||||||
|
count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
first_seen TEXT NOT NULL,
|
||||||
|
last_seen TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS prompt_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp TEXT NOT NULL,
|
||||||
|
class_types TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_node_usage_package ON node_usage(package);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_prompt_log_timestamp ON prompt_log(timestamp);
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class UsageTracker:
|
||||||
|
def __init__(self, db_path=DB_PATH):
|
||||||
|
self._db_path = db_path
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._init_db()
|
||||||
|
|
||||||
|
def _init_db(self):
|
||||||
|
with self._lock:
|
||||||
|
conn = sqlite3.connect(self._db_path)
|
||||||
|
try:
|
||||||
|
conn.executescript(SCHEMA)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def _connect(self):
|
||||||
|
return sqlite3.connect(self._db_path)
|
||||||
|
|
||||||
|
def record_usage(self, class_types, mapper):
|
||||||
|
"""Record usage of a set of class_types from a single prompt execution."""
|
||||||
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
|
with self._lock:
|
||||||
|
conn = self._connect()
|
||||||
|
try:
|
||||||
|
for ct in class_types:
|
||||||
|
package = mapper.get_package(ct)
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO node_usage (class_type, package, count, first_seen, last_seen)
|
||||||
|
VALUES (?, ?, 1, ?, ?)
|
||||||
|
ON CONFLICT(class_type) DO UPDATE SET
|
||||||
|
count = count + 1,
|
||||||
|
last_seen = excluded.last_seen""",
|
||||||
|
(ct, package, now, now),
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO prompt_log (timestamp, class_types) VALUES (?, ?)",
|
||||||
|
(now, json.dumps(list(class_types))),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_node_stats(self):
|
||||||
|
"""Return raw per-node usage data."""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._connect()
|
||||||
|
try:
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT class_type, package, count, first_seen, last_seen FROM node_usage ORDER BY count DESC"
|
||||||
|
).fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_package_stats(self, mapper):
|
||||||
|
"""Aggregate per-package stats combining DB data with known nodes."""
|
||||||
|
node_stats = self.get_node_stats()
|
||||||
|
|
||||||
|
# Build per-package data from DB
|
||||||
|
packages = {}
|
||||||
|
for row in node_stats:
|
||||||
|
pkg = row["package"]
|
||||||
|
if pkg not in packages:
|
||||||
|
packages[pkg] = {
|
||||||
|
"package": pkg,
|
||||||
|
"total_executions": 0,
|
||||||
|
"used_nodes": 0,
|
||||||
|
"nodes": [],
|
||||||
|
"last_seen": None,
|
||||||
|
}
|
||||||
|
entry = packages[pkg]
|
||||||
|
entry["total_executions"] += row["count"]
|
||||||
|
entry["used_nodes"] += 1
|
||||||
|
entry["nodes"].append(row)
|
||||||
|
if entry["last_seen"] is None or row["last_seen"] > entry["last_seen"]:
|
||||||
|
entry["last_seen"] = row["last_seen"]
|
||||||
|
|
||||||
|
# Count total registered nodes per package from mapper
|
||||||
|
node_counts = {}
|
||||||
|
for ct, pkg in mapper.mapping.items():
|
||||||
|
node_counts.setdefault(pkg, 0)
|
||||||
|
node_counts[pkg] += 1
|
||||||
|
|
||||||
|
# Also include zero-node packages from LOADED_MODULE_DIRS
|
||||||
|
for pkg in mapper.get_all_packages():
|
||||||
|
if pkg not in node_counts:
|
||||||
|
node_counts[pkg] = 0
|
||||||
|
|
||||||
|
# Merge: ensure every known package appears
|
||||||
|
for pkg, total in node_counts.items():
|
||||||
|
if pkg not in packages:
|
||||||
|
packages[pkg] = {
|
||||||
|
"package": pkg,
|
||||||
|
"total_executions": 0,
|
||||||
|
"used_nodes": 0,
|
||||||
|
"nodes": [],
|
||||||
|
"last_seen": None,
|
||||||
|
}
|
||||||
|
packages[pkg]["total_nodes"] = total
|
||||||
|
packages[pkg]["never_used"] = packages[pkg]["total_executions"] == 0
|
||||||
|
|
||||||
|
# For packages that came only from DB (e.g. uninstalled), fill total_nodes
|
||||||
|
for pkg, entry in packages.items():
|
||||||
|
if "total_nodes" not in entry:
|
||||||
|
entry["total_nodes"] = entry["used_nodes"]
|
||||||
|
entry["never_used"] = False
|
||||||
|
|
||||||
|
result = sorted(packages.values(), key=lambda p: p["total_executions"])
|
||||||
|
return result
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Clear all tracked data."""
|
||||||
|
with self._lock:
|
||||||
|
conn = self._connect()
|
||||||
|
try:
|
||||||
|
conn.execute("DELETE FROM node_usage")
|
||||||
|
conn.execute("DELETE FROM prompt_log")
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
Reference in New Issue
Block a user