Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions llama/addon/globals/addonLog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

AddonThreadSafeLogCallbackFunction addonThreadSafeLoggerCallback;
bool addonJsLoggerCallbackSet = false;
AddonThreadSafeLogCallbackFunctionContext* addonCurrentLoggerContext = nullptr;
std::atomic<int> addonLoggerLogLevel(5);
std::atomic<std::optional<int>> addonLoggerLogLevelOverride(std::nullopt);
std::atomic<int> addonLastLoggerLogLevel(6);
Expand Down Expand Up @@ -103,17 +104,26 @@ void addonLlamaCppLogCallback(ggml_log_level level, const char* text, void* user
}

Napi::Value setLogger(const Napi::CallbackInfo& info) {
if (addonJsLoggerCallbackSet) {
addonJsLoggerCallbackSet = false;
addonThreadSafeLoggerCallback.Release();
}

if (info.Length() < 1 || !info[0].IsFunction()) {
if (addonJsLoggerCallbackSet) {
addonJsLoggerCallbackSet = false;
addonCurrentLoggerContext = nullptr;
addonThreadSafeLoggerCallback.Release();
}

return info.Env().Undefined();
}

auto addonLoggerJSCallback = info[0].As<Napi::Function>();
AddonThreadSafeLogCallbackFunctionContext* context = new Napi::Reference<Napi::Value>(Napi::Persistent(info.This()));

// install the new logger before releasing the previous one, so that the previous logger's
// deferred finalizer cannot disable the new logger (the finalizer only disables logging
// when its context is still the current one)
const bool hadPreviousLogger = addonJsLoggerCallbackSet;
auto previousLoggerCallback = addonThreadSafeLoggerCallback;

addonCurrentLoggerContext = context;
addonThreadSafeLoggerCallback = AddonThreadSafeLogCallbackFunction::New(
info.Env(),
addonLoggerJSCallback,
Expand All @@ -122,7 +132,10 @@ Napi::Value setLogger(const Napi::CallbackInfo& info) {
1,
context,
[](Napi::Env, void*, AddonThreadSafeLogCallbackFunctionContext* ctx) {
addonJsLoggerCallbackSet = false;
if (addonCurrentLoggerContext == ctx) {
addonJsLoggerCallbackSet = false;
addonCurrentLoggerContext = nullptr;
}

delete ctx;
}
Expand All @@ -132,6 +145,9 @@ Napi::Value setLogger(const Napi::CallbackInfo& info) {
// prevent blocking the main node process from exiting due to active resources
addonThreadSafeLoggerCallback.Unref(info.Env());

if (hadPreviousLogger)
previousLoggerCallback.Release();

return info.Env().Undefined();
}

Expand Down
38 changes: 38 additions & 0 deletions test/modelDependent/llama/logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {describe, expect, test} from "vitest";
import {getModelFile} from "../../utils/modelFiles.js";
import {getLlama, LlamaLogLevel} from "../../../src/index.js";

describe("llama", () => {
describe("logger", () => {
test("a custom logger keeps receiving llama.cpp log lines after a second getLlama() call", {timeout: 1000 * 60 * 60 * 2}, async () => {
const modelPath = await getModelFile("bge-small-en-v1.5-q8_0.gguf");
const lines1: string[] = [];
const lines2: string[] = [];

const llama1 = await getLlama({
logLevel: LlamaLogLevel.debug,
logger(level, message) {
lines1.push(message);
}
});
await llama1.loadModel({modelPath});

const llama2 = await getLlama({
logLevel: LlamaLogLevel.debug,
logger(level, message) {
lines2.push(message);
}
});
await new Promise((resolve) => setTimeout(resolve, 100));
await llama2.loadModel({modelPath});

// llama.cpp emits native log lines through the logger of the most recently created instance.
// Only TS-side logs would reach `lines2` if the native log sink was broken.
expect(lines1.some((line) => line.includes("llama_model_loader"))).toBe(true);
expect(lines2.some((line) => line.includes("llama_model_loader"))).toBe(true);

await llama2.dispose();
await llama1.dispose();
});
});
});