Skip to content

Fixed leak of NLLanguageRecognizer on worker thread exit. - #36

Open
haikesan wants to merge 1 commit into
desktop-app:masterfrom
haikesan:fix/mac-language-recognizer-leak
Open

Fixed leak of NLLanguageRecognizer on worker thread exit.#36
haikesan wants to merge 1 commit into
desktop-app:masterfrom
haikesan:fix/mac-language-recognizer-leak

Conversation

@haikesan

Copy link
Copy Markdown

Platform::Language::Recognize keeps one NLLanguageRecognizer per thread in a static thread_local raw pointer (since c8ded8b7). Nothing ever releases it, so every time a thread that called Recognize exits, its recognizer is leaked together with the CoreML model it holds (MontrealNeuralNetwork / Espresso::net).

On macOS Spellchecker::CheckSpelling calls Recognize for every word, and it runs on crl::async worker threads (platform_spellcheck_async.cpp). GCD spins those threads up and tears them down all the time, so the leak accumulates for as long as the app lives.

Observed on a Telegram Desktop build after ~2 days of uptime, leaks:

Process 7367: 46021 leaks for 26371392 total leaked bytes.
1252x ROOT LEAK: <NLLanguageRecognizer ...>

and heap shows the matching 1441 std::__shared_ptr_emplace<Espresso::net> / 1439 MontrealNeuralNetwork instances still alive.

Standalone reproduction (200 short-lived threads, each calls Recognize once):
before - 15254 leaks for 13153648 total leaked bytes, 200x ROOT LEAK: <NLLanguageRecognizer>
after - 0 leaks for 0 total leaked bytes

The fix keeps the per-thread instance (same design as the cld3 thread_local in 3a47097) but wraps it in a small RAII holder, so the thread-local destructor releases the recognizer when the thread exits.

repro.mm

// clang++ -std=c++20 -fno-objc-arc -DFIXED=0|1 repro.mm -framework Foundation -framework NaturalLanguage
#import <Foundation/Foundation.h>
#import <NaturalLanguage/NLLanguageRecognizer.h>
#include <thread>
#include <unistd.h>

static void Recognize(NSString *text) {
#if FIXED
	struct Recognizer {
		NLLanguageRecognizer *r = [[NLLanguageRecognizer alloc] init];
		~Recognizer() { [r release]; }
	};
	static thread_local Recognizer holder;
	const auto r = holder.r;
#else
	static thread_local auto r = [] { return [[NLLanguageRecognizer alloc] init]; }();
#endif
	[r processString:text];
	(void)[r languageHypothesesWithMaximum:3];
	[r reset];
}

int main() {
	for (int i = 0; i < 200; ++i) {
		std::thread([] { @autoreleasepool { Recognize(@"hello world spelling"); } }).join();
	}
	printf("pid=%d\n", getpid()); fflush(stdout);
	pause(); // now run: leaks <pid>
}

@CLAassistant

CLAassistant commented Aug 27, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants