Skip to content
Merged
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
9 changes: 5 additions & 4 deletions js/node/src/inference_session_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "common.h"
#include "inference_session_wrap.h"
#include "ort_instance_data.h"
#include "ort_singleton_data.h"

Check warning on line 9 in js/node/src/inference_session_wrap.cc

View workflow job for this annotation

GitHub Actions / Optional Lint C++

[cpplint] reported by reviewdog 🐶 Include the directory when naming header files [build/include_subdir] [4] Raw Output: js/node/src/inference_session_wrap.cc:9: Include the directory when naming header files [build/include_subdir] [4]
#include "run_options_helper.h"
#include "session_options_helper.h"
#include "tensor_helper.h"
Expand Down Expand Up @@ -76,7 +77,7 @@
Napi::String value = info[0].As<Napi::String>();

ParseSessionOptions(info[1].As<Napi::Object>(), sessionOptions);
this->session_.reset(new Ort::Session(*OrtInstanceData::OrtEnv(),
this->session_.reset(new Ort::Session(OrtSingletonData::Env(),
#ifdef _WIN32
reinterpret_cast<const wchar_t*>(value.Utf16Value().c_str()),
#else
Expand All @@ -91,7 +92,7 @@
int64_t bytesLength = info[2].As<Napi::Number>().Int64Value();

ParseSessionOptions(info[3].As<Napi::Object>(), sessionOptions);
this->session_.reset(new Ort::Session(*OrtInstanceData::OrtEnv(),
this->session_.reset(new Ort::Session(OrtSingletonData::Env(),
reinterpret_cast<char*>(buffer) + bytesOffset, bytesLength,
sessionOptions));
} else {
Expand Down Expand Up @@ -211,7 +212,7 @@
ParseRunOptions(info[2].As<Napi::Object>(), runOptions);
}
if (preferredOutputLocations_.size() == 0) {
session_->Run(runOptions == nullptr ? *OrtInstanceData::OrtDefaultRunOptions() : runOptions,
session_->Run(runOptions == nullptr ? OrtSingletonData::DefaultRunOptions() : runOptions,
inputIndex == 0 ? nullptr : &inputNames_cstr[0], inputIndex == 0 ? nullptr : &inputValues[0],
inputIndex, outputIndex == 0 ? nullptr : &outputNames_cstr[0],
outputIndex == 0 ? nullptr : &outputValues[0], outputIndex);
Expand Down Expand Up @@ -240,7 +241,7 @@
}
}

session_->Run(runOptions == nullptr ? *OrtInstanceData::OrtDefaultRunOptions() : runOptions, *ioBinding_);
session_->Run(runOptions == nullptr ? OrtSingletonData::DefaultRunOptions() : runOptions, *ioBinding_);

auto outputs = ioBinding_->GetOutputValues();
ORT_NAPI_THROW_ERROR_IF(outputs.size() != outputIndex, env, "Output count mismatch.");
Expand Down
29 changes: 3 additions & 26 deletions js/node/src/ort_instance_data.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,10 @@

#include "common.h"
#include "ort_instance_data.h"
#include "ort_singleton_data.h"

Check warning on line 9 in js/node/src/ort_instance_data.cc

View workflow job for this annotation

GitHub Actions / Optional Lint C++

[cpplint] reported by reviewdog 🐶 Include the directory when naming header files [build/include_subdir] [4] Raw Output: js/node/src/ort_instance_data.cc:9: Include the directory when naming header files [build/include_subdir] [4]
#include "onnxruntime_cxx_api.h"

std::unique_ptr<Ort::Env> OrtInstanceData::ortEnv;
std::unique_ptr<Ort::RunOptions> OrtInstanceData::ortDefaultRunOptions;
std::mutex OrtInstanceData::ortEnvMutex;
std::atomic<uint64_t> OrtInstanceData::ortEnvRefCount;
std::atomic<bool> OrtInstanceData::ortEnvDestroyed;

OrtInstanceData::OrtInstanceData() {
++ortEnvRefCount;
}

OrtInstanceData::~OrtInstanceData() {
if (--ortEnvRefCount == 0) {
std::lock_guard<std::mutex> lock(ortEnvMutex);
if (ortEnv) {
ortDefaultRunOptions.reset(nullptr);
ortEnv.reset();
ortEnvDestroyed = true;
}
}
}

void OrtInstanceData::Create(Napi::Env env, Napi::Function inferenceSessionWrapperFunction) {
Expand All @@ -42,14 +25,8 @@

data->ortTensorConstructor = Napi::Persistent(tensorConstructor);

if (!ortEnv) {
std::lock_guard<std::mutex> lock(ortEnvMutex);
if (!ortEnv) {
ORT_NAPI_THROW_ERROR_IF(ortEnvDestroyed, env, "OrtEnv already destroyed.");
ortEnv.reset(new Ort::Env{OrtLoggingLevel(log_level), "onnxruntime-node"});
ortDefaultRunOptions.reset(new Ort::RunOptions{});
}
}
// Only the first time call to OrtSingletonData::GetOrCreateOrtObjects() will create the Ort::Env
OrtSingletonData::GetOrCreateOrtObjects(log_level);
}

const Napi::FunctionReference& OrtInstanceData::TensorConstructor(Napi::Env env) {
Expand Down
18 changes: 0 additions & 18 deletions js/node/src/ort_instance_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@

/**
* The OrtInstanceData class is designed to manage the lifecycle of necessary instance data, including:
* - The Ort::Env singleton instance.
* This is a global singleton that is shared across all InferenceSessionWrap instances. It is created when the first
* time `InferenceSession.initOrtOnce()` is called. It is destroyed when the last active NAPI Env is destroyed.
* Once destroyed, it cannot be created again.
*
* - The Object reference of the InferenceSessionWrap class and the Tensor constructor.
* This is a per-env data that has the same lifecycle as the Napi::Env. If there are worker threads, each thread will
* have its own handle to the InferenceSessionWrap class and the Tensor constructor.
Expand All @@ -27,24 +22,11 @@ struct OrtInstanceData {
static void InitOrt(Napi::Env env, int log_level, Napi::Function tensorConstructor);
// Get the Tensor constructor reference for the Napi::Env
static const Napi::FunctionReference& TensorConstructor(Napi::Env env);
// Get the global Ort::Env
static const Ort::Env* OrtEnv() { return ortEnv.get(); }
// Get the default Ort::RunOptions
static Ort::RunOptions* OrtDefaultRunOptions() { return ortDefaultRunOptions.get(); }

~OrtInstanceData();

private:
OrtInstanceData();

// per env persistent constructors
Napi::FunctionReference wrappedSessionConstructor;
Napi::FunctionReference ortTensorConstructor;

// ORT env (global singleton)
static std::unique_ptr<Ort::Env> ortEnv;
static std::unique_ptr<Ort::RunOptions> ortDefaultRunOptions;
static std::mutex ortEnvMutex;
static std::atomic<uint64_t> ortEnvRefCount;
static std::atomic<bool> ortEnvDestroyed;
};
22 changes: 22 additions & 0 deletions js/node/src/ort_singleton_data.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#include "ort_singleton_data.h"

Check warning on line 4 in js/node/src/ort_singleton_data.cc

View workflow job for this annotation

GitHub Actions / Optional Lint C++

[cpplint] reported by reviewdog 🐶 Include the directory when naming header files [build/include_subdir] [4] Raw Output: js/node/src/ort_singleton_data.cc:4: Include the directory when naming header files [build/include_subdir] [4]

OrtSingletonData::OrtObjects::OrtObjects(int log_level)
: env{OrtLoggingLevel(log_level), "onnxruntime-node"},
default_run_options{} {
}

OrtSingletonData::OrtObjects& OrtSingletonData::GetOrCreateOrtObjects(int log_level) {
static OrtObjects ort_objects(log_level);
return ort_objects;
}

const Ort::Env& OrtSingletonData::Env() {
return GetOrCreateOrtObjects().env;
}

const Ort::RunOptions& OrtSingletonData::DefaultRunOptions() {
return GetOrCreateOrtObjects().default_run_options;
}
40 changes: 40 additions & 0 deletions js/node/src/ort_singleton_data.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#pragma once

#include <napi.h>
#include "onnxruntime_cxx_api.h"

Check warning on line 7 in js/node/src/ort_singleton_data.h

View workflow job for this annotation

GitHub Actions / Optional Lint C++

[cpplint] reported by reviewdog 🐶 Include the directory when naming header files [build/include_subdir] [4] Raw Output: js/node/src/ort_singleton_data.h:7: Include the directory when naming header files [build/include_subdir] [4]

/**
* The OrtSingletonData class is designed to manage the lifecycle of necessary singleton instance data, including:
*
* - The Ort::Env singleton instance.
* This is a global singleton that is shared across all InferenceSessionWrap instances. It is created when the first
* time `InferenceSession.initOrtOnce()` is called.
*
* - The Ort::RunOptions singleton instance.
* This is an empty default RunOptions instance. It is created once to allow reuse across all session inference runs.
*
* The OrtSingletonData class uses the "Meyers Singleton" pattern to ensure thread-safe lazy initialization, as well as
* proper destruction order at program exit.
*/
struct OrtSingletonData {
struct OrtObjects {
Ort::Env env;
Ort::RunOptions default_run_options;

private:
// The following pattern ensures that OrtObjects can only be created by OrtSingletonData
OrtObjects(int log_level);

Check warning on line 29 in js/node/src/ort_singleton_data.h

View workflow job for this annotation

GitHub Actions / Optional Lint C++

[cpplint] reported by reviewdog 🐶 Single-parameter constructors should be marked explicit. [runtime/explicit] [4] Raw Output: js/node/src/ort_singleton_data.h:29: Single-parameter constructors should be marked explicit. [runtime/explicit] [4]
friend struct OrtSingletonData;
};

static OrtObjects& GetOrCreateOrtObjects(int log_level = ORT_LOGGING_LEVEL_WARNING);

// Get the global Ort::Env
static const Ort::Env& Env();

// Get the default Ort::RunOptions
static const Ort::RunOptions& DefaultRunOptions();
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as path from 'path';

import { assertTensorEqual, SQUEEZENET_INPUT0_DATA, SQUEEZENET_OUTPUT0_DATA, TEST_DATA_ROOT } from '../test-utils';

describe('E2E Tests - InferenceSession.run()', async () => {
describe('API Tests - InferenceSession.run()', async () => {
let session: InferenceSession;
const input0 = new Tensor('float32', SQUEEZENET_INPUT0_DATA, [1, 3, 224, 224]);
const expectedOutput0 = new Tensor('float32', SQUEEZENET_OUTPUT0_DATA, [1, 1000, 1, 1]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const MODEL_TEST_TYPES_CASES: Array<{
},
];

describe('E2E Tests - simple E2E tests', () => {
describe('API Tests - simple API tests', () => {
MODEL_TEST_TYPES_CASES.forEach((testCase) => {
it(`${testCase.model}`, async () => {
const session = await InferenceSession.create(testCase.model);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { assertTensorEqual, SQUEEZENET_INPUT0_DATA, SQUEEZENET_OUTPUT0_DATA, TES
import * as path from 'path';

if (isMainThread) {
describe('E2E Tests - worker test', () => {
describe('API Tests - worker test', () => {
it('should run in worker', (done) => {
const worker = new Worker(__filename, {
stdout: true,
Expand Down
23 changes: 23 additions & 0 deletions js/node/test/standalone/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Process Exit Tests

These tests verify that the ORT Node.js binding handles various process exit scenarios without crashing, specifically addressing the mutex crash issue reported in [#24579](https://github.com/microsoft/onnxruntime/issues/24579).

## What This Tests

- **Normal process exit** - Verifies clean shutdown without mutex crashes
- **`process.exit()` calls** - Tests the primary crash scenario that was fixed
- **Uncaught exceptions** - Ensures crashes don't occur during unexpected exits
- **Session cleanup** - Tests both explicit `session.release()` and automatic cleanup
- **Stability** - Multiple runs to ensure consistent behavior

## How It Works

Each test runs in a separate Node.js process to isolate the test environment. Tests use command-line flags to control behavior:

- `--process-exit`: Triggers `process.exit(0)`
- `--throw-exception`: Throws an uncaught exception
- `--release`: Calls `session.release()` before exit

## Expected Result

All tests should pass without `mutex lock failed` or `std::__1::system_error` messages in stderr.
86 changes: 86 additions & 0 deletions js/node/test/standalone/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { spawn } from 'child_process';
import * as assert from 'assert';
import * as path from 'path';

describe('Standalone Process Tests', () => {
// Helper function to run test script in a separate process
const runTest = async (args: string[] = []): Promise<{ code: number; stdout: string; stderr: string }> =>
new Promise((resolve, reject) => {
// Use the compiled main.js file from the lib directory
const testFile = path.join(__dirname, './main.js');

const child = spawn('node', [testFile, ...args], { stdio: 'pipe' });

let stdout = '';
let stderr = '';

child.stdout.on('data', (data) => (stdout += data.toString()));
child.stderr.on('data', (data) => (stderr += data.toString()));

child.on('close', (code) => {
resolve({ code: code || 0, stdout, stderr });
});

child.on('error', reject);
});

// Helper function to check basic success criteria
const assertSuccess = (result: { code: number; stdout: string; stderr: string }) => {
assert.strictEqual(result.code, 0);
assert.ok(result.stdout.includes('SUCCESS: Inference completed'));
assert.ok(!result.stderr.includes('mutex lock failed'));
};

// Helper function to check that no mutex crashes occurred
const assertNoMutexErrors = (stderr: string) => {
assert.ok(!stderr.includes('mutex lock failed'));
assert.ok(!stderr.includes('std::__1::system_error'));
};

it('should handle normal process exit', async () => {
const result = await runTest([]);
assertSuccess(result);
});

it('should handle process.exit() call', async () => {
const result = await runTest(['--process-exit']);
assertSuccess(result);
});

it('should handle uncaught exceptions', async () => {
const result = await runTest(['--throw-exception']);

assert.notStrictEqual(result.code, 0);
assert.ok(result.stdout.includes('SUCCESS: Inference completed'));
assert.ok(result.stderr.includes('Test exception'));
assertNoMutexErrors(result.stderr);
});

it('should handle multiple process exits consistently', async () => {
for (let i = 0; i < 3; i++) {
const result = await runTest(['--process-exit']);
assertSuccess(result);
}
});

it('should handle session.release() before normal exit', async () => {
const result = await runTest(['--release']);
assertSuccess(result);
assert.ok(result.stdout.includes('Session released'));
});

it('should handle session.release() before process.exit()', async () => {
const result = await runTest(['--release', '--process-exit']);
assertSuccess(result);
assert.ok(result.stdout.includes('Session released'));
});

it('should handle no session.release() before process.exit()', async () => {
const result = await runTest(['--process-exit']);
assertSuccess(result);
assert.ok(result.stdout.includes('Session NOT released'));
});
});
51 changes: 51 additions & 0 deletions js/node/test/standalone/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import * as path from 'path';
const ort = require(path.join(__dirname, '../../'));
import * as process from 'process';

const modelData =
'CAMSDGJhY2tlbmQtdGVzdDpiChEKAWEKAWISAWMiBk1hdE11bBIOdGVzdF9tYXRtdWxfMmRaEwoBYRIOCgwIARIICgIIAwoCCARaEwoBYhIOCgwIARIICgIIBAoCCANiEwoBYxIOCgwIARIICgIIAwoCCANCAhAJ';
const shouldProcessExit = process.argv.includes('--process-exit');
const shouldThrowException = process.argv.includes('--throw-exception');
const shouldRelease = process.argv.includes('--release');

async function main() {
try {
const modelBuffer = Buffer.from(modelData, 'base64');
const session = await ort.InferenceSession.create(modelBuffer);

const dataA = Float32Array.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
const dataB = Float32Array.from([10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]);
const tensorA = new ort.Tensor('float32', dataA, [3, 4]);
const tensorB = new ort.Tensor('float32', dataB, [4, 3]);

const results = await session.run({ a: tensorA, b: tensorB });
console.log('SUCCESS: Inference completed');
console.log(`Result: ${results.c.data}`);

if (shouldRelease) {
await session.release();
console.log('Session released');
} else {
console.log('Session NOT released (testing cleanup behavior)');
}

if (shouldThrowException) {
setTimeout(() => {
throw new Error('Test exception');
}, 10);
return;
}

if (shouldProcessExit) {
process.exit(0);
}
} catch (e) {
console.error(`ERROR: ${e}`);
process.exit(1);
}
}

void main();
11 changes: 7 additions & 4 deletions js/node/test/test-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ require('./unittests/lib/inference-session');
require('./unittests/lib/model-metadata');
require('./unittests/lib/tensor');

// E2E tests
require('./e2e/simple-e2e-tests');
require('./e2e/inference-session-run');
require('./e2e/worker-test');
// API tests
require('./api/simple-api-tests');
require('./api/inference-session-run');
require('./api/worker-test');

// standalone tests
require('./standalone/index');

// Test ONNX spec tests
import { run as runTestRunner } from './test-runner';
Expand Down
Loading