-
Notifications
You must be signed in to change notification settings - Fork 29
feat(major): [sc-23696] replace jemalloc dependency with custom malloc interposer #333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat(major): [sc-23696] replace jemalloc dependency with custom malloc interposer #333
Conversation
hassila
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some initial feedback, will ping you offline about the initial bootstrap problem...
| /// Number of total mallocs | ||
| case mallocCountTotal | ||
| /// Number of totatl free calls | ||
| case freeCountTotal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should keep the small and large malloc count metrics, I would suggest that we put the threshold for what is large/small to match the page size (and document it as such, so e.g. on macOS/ARM it'd be 16KB, while on Linux x86 it'd be 4K). Removing these metrics would be unnecessarily source breaking.
|
|
||
| let parameterization = (0...5).map { 1 << $0 } // 1, 2, 4, ... | ||
| let parameterization = (0...5).map { 1 << $0 } // 1, 2, 4, ... | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please merge in .swift-format from main and apply swift-format, then we can remove a lot of these diffs with extra spaces.
| @@ -0,0 +1,88 @@ | |||
| #ifndef INTERPOSER_H | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general for all new source files, add Apache license header:
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//| @@ -0,0 +1,24 @@ | |||
| // swift-tools-version: 6.1 | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should move to "swift-tools-version: 5.10" everywhere, as we still support 5.10.
|
|
||
| if mallocStatsRequested { | ||
| startMallocStats = MallocStatsProducer.makeMallocStats() | ||
| MallocInterposerSwift.hook() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't want to hook in this closure, it can be called twice as mentioned in the above comment - it is also unnecessary overhead to hook/unhook for each benchmark iteration, better to do the hooking outside of the loop - see how it's done for ARCStatsProducer which also hooks/unhooks, but does it outside the loop.
|
|
||
| if mallocStatsRequested { | ||
| stopMallocStats = MallocStatsProducer.makeMallocStats() | ||
| MallocInterposerSwift.unhook() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, you want to unhook outside of the loop...
|
Also I see a difference in which metrics are output between main and this branch, e.g. : Main: this PR: Got some additional malloc related metrics there, would be good to understand root cause. |
|
(that was from running |
|
Also one issue when testing with package-benchmarks-samples repo with lost capture of allocation: Main gives: PR: |
|
(Memory (allocated resident) (K) was missing from the PR output too?) |
|
This test also misses capture: PR: |
|
And this test should show a leak, but does not on the PR: PR: |
Thanks for the feedback! That's strange will look into it.. |
|
This one failed to find the Malloc/free delta leak on Linux: |
|
For Linux: cp .build/x86_64-unknown-linux-gnu/debug/libMallocInterposerC-tool.so .build/x86_64-unknown-linux-gnu/debug/libMallocInterposerC.so
cp .build/x86_64-unknown-linux-gnu/debug/libMallocInterposerSwift-tool.so .build/x86_64-unknown-linux-gnu/debug/libMallocInterposerSwift.so(need to rebuild in between too, so build, first copy, build, second copy, ...) |
|
Fixes we need before merge: |
|
The fixes got merged! We can now:
and it will work as expected. I reached out to Owen from SPM team and asked him to help us to get the fixed cherry picked in a future release. |
|
I will take the time to cherry pick these into a future release. The cherry pick could be a bit tricky because we depend on a lot of changes which are not cherry picked. |
| void *replacement_reallocf(void *ptr, size_t size) { | ||
| void *new_ptr = replacement_realloc(ptr, size); | ||
| if (!new_ptr) { | ||
| replacement_free(new_ptr); | ||
| } | ||
| return new_ptr; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical bug in replacement_reallocf: When realloc fails, it should free the original pointer (ptr), not new_ptr (which is NULL). The function reallocf is designed to free the original memory on failure to prevent memory leaks.
void *replacement_reallocf(void *ptr, size_t size) {
void *new_ptr = replacement_realloc(ptr, size);
if (!new_ptr) {
replacement_free(ptr); // Should free ptr, not new_ptr
}
return new_ptr;
}| void *replacement_reallocf(void *ptr, size_t size) { | |
| void *new_ptr = replacement_realloc(ptr, size); | |
| if (!new_ptr) { | |
| replacement_free(new_ptr); | |
| } | |
| return new_ptr; | |
| } | |
| void *replacement_reallocf(void *ptr, size_t size) { | |
| void *new_ptr = replacement_realloc(ptr, size); | |
| if (!new_ptr) { | |
| replacement_free(ptr); | |
| } | |
| return new_ptr; | |
| } |
Spotted by Graphite Agent
Is this helpful? React 👍 or 👎 to let us know.
| linkerSettings: [ | ||
| .linkedLibrary("dl") | ||
| ]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dl library is Linux-specific and doesn't exist on macOS. This will cause build failures on macOS. The linker setting should be conditional:
linkerSettings: [
.linkedLibrary("dl", .when(platforms: [.linux]))
]| linkerSettings: [ | |
| .linkedLibrary("dl") | |
| ]) | |
| linkerSettings: [ | |
| .linkedLibrary("dl", .when(platforms: [.linux])) | |
| ]) |
Spotted by Graphite Agent
Is this helpful? React 👍 or 👎 to let us know.
|
Great news is that all the dependencies we need are merged in for the next release of swift package manager (6.3.0) which should hopefully come out in March! We can test it by:
|
Description
This PR replaces
jemallocwith a custom malloc interposer. The interposer logic is mostly inspired by the great work done by theswift-nioteam.On macOS/Darwin we ulitise the interposing feature of dydl while on Linux we use
LD_PRELOADin order to interpose.The interposition logic is written in C and located in the
MallocInterposerCpackage.For actual interaction with the interposition is done through a wrapper package
MallocInterposerSwift, which exposes a simple interface tohookandunhookthe interposition logic and also to actually count the statistics we need.The interposition requirers the the interposer library to be linked dynamically, since that's crucial to the nature of how the interposition works (both on Linux and Darwin). The SwiftPM Command plugin
BenchmarkCommandPluginhas a dependency onBenchmarkwhich has a dependency on the interposer libs. This causes a problem for SwiftPM and intitial tiggers ofswift package benchmarkcommand will fail.The issue is described in more detail here.
The current workaround is to trigger the
swift package benchmarkcommand once and let it fail. Then to copy thelibMallocInterposerC-tool.dylibandlibMallocInterposerSwift-tool.dyliband name them without the-toolsuffix.e.g.
The same applies for Linux but the build location will be different and the dynamic library extension is
.so.Since jemalloc is removed the
mallocSmallandmallocLargemetrics are removed. New metricsmallocByteCountandfreeTotalCountare introducted.How Has This Been Tested?
When running the test the executable is being linked statically to the test target. I didn't discover a way yet to dynamically link the interposer in order for it to work inside tests.
Minimal checklist:
DocCcode-level documentation for any public interfaces exported by the package