SERVER-126049: Use thread-local counter for mozjs mmap allocations (#53369)

GitOrigin-RevId: 27133c87a3234957a537cc0e82a983aba704b593
This commit is contained in:
Anna Veselova 2026-05-08 13:43:17 -05:00 committed by MongoDB Bot
parent e67d85fc61
commit eb8092bf50
15 changed files with 416 additions and 24 deletions

View File

@ -149,7 +149,7 @@
},
{
"name": "import_script_path",
"value": "src/third_party/mozjs/get-sources.sh"
"value": "src/third_party/mozjs/scripts/import.sh"
}
]
},
@ -3791,4 +3791,4 @@
"dependsOn": []
}
]
}
}

View File

@ -0,0 +1,264 @@
/**
* Verifies that the per-scope JS heap limit (jsHeapLimitMB) does not produce false OOM errors
* under concurrency, scope pool reuse, and mixed allocation patterns. Both
* jsUseLegacyMemoryTracking modes must produce zero OOM errors.
*
* @tags: [
* requires_scripting,
* mozjs_wasm_unsupported,
* ]
*/
import {Thread} from "jstests/libs/parallelTester.js";
const JS_HEAP_LIMIT_MB = 50;
const conn = MongoRunner.runMongod({setParameter: {jsHeapLimitMB: JS_HEAP_LIMIT_MB}});
const testDB = conn.getDB("js_mozjs_heap_limit");
const coll = testDB.coll;
assert.commandWorked(coll.insertMany(Array.from({length: 100}, (_, i) => ({_id: i, n: 0}))));
// ---------------------------------------------------------------------------
// Concurrent $where
//
// Each $where evaluation checks out a MozJSImplScope from the scope pool,
// which owns a full SpiderMonkey JSContext. SpiderMonkey backs its GC arena
// with mmap'd 1 MB chunks (MapAlignedPages -> record_mmap_alloc); a scope
// evaluating a trivial predicate like this one uses ~5 MB of mmap.
//
// Overcounting risk: if the mmap counter is not scoped per-thread, concurrent
// scopes inflate each other's apparent usage. With 16 threads each holding
// ~5 MB of GC arena, a shared counter would show ~80 MB per scope -- well
// over the 50 MB jsHeapLimitMB -- causing ExceededMemoryLimit on scope init
// even though each individual scope is using only ~5 MB (10% of the limit).
// ---------------------------------------------------------------------------
function concurrentWorker(host, dbName, iterations) {
const db = new Mongo(host).getDB(dbName);
let oom = 0,
other = 0,
ok = 0;
for (let i = 0; i < iterations; i++) {
try {
const res = db.runCommand({
findAndModify: "coll",
query: {_id: Math.floor(Math.random() * 100), $where: "this._id !== undefined"},
update: {$inc: {n: 1}},
});
res.ok === 1
? ok++
: res.code === ErrorCodes.ExceededMemoryLimit || res.code === ErrorCodes.JSInterpreterFailure
? oom++
: other++;
} catch (e) {
e.code === ErrorCodes.ExceededMemoryLimit || e.code === ErrorCodes.JSInterpreterFailure ? oom++ : other++;
}
}
return {oom, other, ok};
}
function runConcurrent(threads, iterations) {
const ts = Array.from(
{length: threads},
() => new Thread(concurrentWorker, conn.host, testDB.getName(), iterations),
);
ts.forEach((t) => t.start());
return ts.reduce(
(acc, t) => {
t.join();
const r = t.returnData();
return {oom: acc.oom + r.oom, other: acc.other + r.other, ok: acc.ok + r.ok};
},
{oom: 0, other: 0, ok: 0},
);
}
for (const legacy of [false, true]) {
jsTest.log.info(`Phase 1: concurrent $where, jsUseLegacyMemoryTracking=${legacy}`);
assert.commandWorked(testDB.adminCommand({setParameter: 1, jsUseLegacyMemoryTracking: legacy}));
const r = runConcurrent(16, 5);
assert.eq(r.oom, 0, `jsUseLegacyMemoryTracking=${legacy}: unexpected OOM errors: ${tojson(r)}`);
assert.eq(r.other, 0, `jsUseLegacyMemoryTracking=${legacy}: unexpected errors: ${tojson(r)}`);
}
// ---------------------------------------------------------------------------
// Scope pool reuse: sequential queries must not accumulate mmap_bytes
//
// $function checks out a scope from MozJSProxyScope's pool and returns it
// after each aggregation completes. new ArrayBuffer(1 MB) allocates its
// backing store through SpiderMonkey's BufferAllocator, which calls mmap
// directly (not the GC arena) and is tracked via record_mmap_alloc. When
// the scope is returned to the pool a GC runs and collects the ArrayBuffer;
// its finalizer calls UnmapPages -> record_mmap_free, driving mmap_bytes
// back to zero before the next checkout.
//
// Overcounting risk: if record_mmap_free is not called when the buffer is
// collected, or if the counter is not correctly associated with the scope's
// thread, mmap_bytes accumulates across reuse cycles. Per-iteration peak is
// ~5 MB. With a 50 MB limit, unchecked accumulation would cross the limit
// after ~9 additional iterations. Running 15 iterations gives clear headroom
// above the failure point while keeping the test fast.
// ---------------------------------------------------------------------------
jsTest.log.info("Phase 2: scope pool reuse, sequential mmap_bytes accumulation check");
assert.commandWorked(testDB.adminCommand({setParameter: 1, jsUseLegacyMemoryTracking: false}));
for (let i = 0; i < 15; i++) {
const res = coll
.aggregate([
{$limit: 1},
{
$project: {
r: {
$function: {
body: function () {
// ArrayBuffer backing store -> BufferAllocator -> mmap -> record_mmap_alloc.
// Freed via UnmapPages -> record_mmap_free when the GC collects this buffer on scope return.
let b = new ArrayBuffer(1024 * 1024);
return new Uint8Array(b)[0];
},
args: [],
lang: "js",
},
},
},
},
])
.toArray();
assert.eq(res.length, 1, `scope reuse iteration ${i} failed`);
}
// ---------------------------------------------------------------------------
// Mixed allocations: arena objects + BufferAllocator mmap in same scope
//
// Exercises two allocation paths within a single $function invocation:
// - 100 plain JS objects {x: j}: GC arena objects, bump-allocated from
// mmap'd arena chunks. Creating many short-lived objects
// triggers GC, exercising the record_mmap_free path as chunks are swept.
// - new Uint8Array(200 * 1024): the 200 KB typed-array backing store is a
// medium BufferAllocator allocation, sub-allocated from a mmap'd
// BufferChunk.
//
// Overcounting risk: if mmap_bytes leaks across scope boundaries under
// concurrent load, the apparent total inflates. Per-scope total is ~5 MB
// << 50 MB limit, so any OOM error indicates a counting error rather than
// genuine memory pressure.
// ---------------------------------------------------------------------------
function mixedWorker(host, dbName, iterations) {
const db = new Mongo(host).getDB(dbName);
let oom = 0,
other = 0;
for (let i = 0; i < iterations; i++) {
try {
db.coll
.aggregate([
{$limit: 1},
{
$project: {
r: {
$function: {
body: function () {
// Plain JS objects: GC arena (malloc_bytes).
let s = [];
for (let j = 0; j < 100; j++) s.push({x: j});
// Typed array backing store: BufferAllocator mmap (mmap_bytes).
return s.length + new Uint8Array(200 * 1024)[0];
},
args: [],
lang: "js",
},
},
},
},
])
.toArray();
} catch (e) {
e.code === ErrorCodes.ExceededMemoryLimit || e.code === ErrorCodes.JSInterpreterFailure ? oom++ : other++;
}
}
return {oom, other};
}
jsTest.log.info("Phase 3: concurrent GC pressure + BufferAllocator mmap");
assert.commandWorked(testDB.adminCommand({setParameter: 1, jsUseLegacyMemoryTracking: false}));
const mixedTs = Array.from({length: 8}, () => new Thread(mixedWorker, conn.host, testDB.getName(), 3));
mixedTs.forEach((t) => t.start());
const mixed = mixedTs.reduce(
(acc, t) => {
t.join();
const r = t.returnData();
return {oom: acc.oom + r.oom, other: acc.other + r.other};
},
{oom: 0, other: 0},
);
assert.eq(mixed.oom, 0, `mixed allocation test: unexpected OOM errors: ${tojson(mixed)}`);
assert.eq(mixed.other, 0, `mixed allocation test: unexpected errors: ${tojson(mixed)}`);
// ---------------------------------------------------------------------------
// malloc_bytes stress: long strings allocated via StringBufferArena
//
// SpiderMonkey stores long string character data in a separate heap buffer
// allocated through StringBufferArena -> js_arena_malloc -> wrap_alloc ->
// malloc_bytes. Strings longer than ~23 chars exceed the inline limit and
// always use this path. 10,000 Latin-1 strings of 1000 chars = ~10 MB of
// malloc_bytes per scope. Combined with the ~5 MB GC arena baseline in
// mmap_bytes, the per-scope total is ~15 MB, well under the 50 MB limit.
//
// Overcounting risk: if malloc_bytes is not scoped per-thread, concurrent
// scopes inflate each other's apparent usage. With 8 threads each holding
// ~10 MB of string data, a shared counter would show ~80 MB per scope --
// far exceeding the 50 MB jsHeapLimitMB.
// ---------------------------------------------------------------------------
function mallocWorker(host, dbName, iterations) {
const db = new Mongo(host).getDB(dbName);
let oom = 0,
other = 0;
for (let i = 0; i < iterations; i++) {
try {
db.coll
.aggregate([
{$limit: 1},
{
$project: {
r: {
$function: {
body: function () {
// Long strings: char data -> StringBufferArena -> js_malloc -> malloc_bytes.
// 10,000 x 1000-char Latin-1 strings = ~10 MB of malloc_bytes.
let strs = [];
for (let j = 0; j < 10000; j++) {
strs.push("x".repeat(1000));
}
return strs.length;
},
args: [],
lang: "js",
},
},
},
},
])
.toArray();
} catch (e) {
e.code === ErrorCodes.ExceededMemoryLimit || e.code === ErrorCodes.JSInterpreterFailure ? oom++ : other++;
}
}
return {oom, other};
}
jsTest.log.info("Phase 4: concurrent malloc_bytes stress via long string allocations");
assert.commandWorked(testDB.adminCommand({setParameter: 1, jsUseLegacyMemoryTracking: false}));
const mallocTs = Array.from({length: 8}, () => new Thread(mallocWorker, conn.host, testDB.getName(), 3));
mallocTs.forEach((t) => t.start());
const mallocResult = mallocTs.reduce(
(acc, t) => {
t.join();
const r = t.returnData();
return {oom: acc.oom + r.oom, other: acc.other + r.other};
},
{oom: 0, other: 0},
);
assert.eq(mallocResult.oom, 0, `malloc stress test: unexpected OOM errors: ${tojson(mallocResult)}`);
assert.eq(mallocResult.other, 0, `malloc stress test: unexpected errors: ${tojson(mallocResult)}`);
MongoRunner.stopMongod(conn);

View File

@ -114,7 +114,7 @@
},
{
"name": "import_script_path",
"value": "src/third_party/mozjs/get-sources.sh"
"value": "src/third_party/mozjs/scripts/import.sh"
}
]
},
@ -2834,4 +2834,4 @@
"dependsOn": []
}
]
}
}

View File

@ -238,5 +238,93 @@ TEST_F(JSCustomAllocatorTest, OOMDuringScopeInitDoesNotCrash) {
}
#endif // MONGO_CONFIG_DEBUG_BUILD
// With tracking disabled, record_mmap_alloc should be a no-op.
TEST_F(JSCustomAllocatorTest, MmapTrackingDisabledByDefault) {
mongo::sm::reset(0, false);
mongo::sm::record_mmap_alloc(1024);
ASSERT_EQUALS(mongo::sm::get_mmap_bytes(), 0);
ASSERT_EQUALS(mongo::sm::get_total_bytes(), 0);
}
// Basic alloc/free round-trip with mmap tracking enabled.
TEST_F(JSCustomAllocatorTest, MmapAllocAndFree) {
// A non-zero limit is required; record_mmap_alloc no-ops when max_bytes == 0.
mongo::sm::reset(10 * 1024 * 1024, true);
mongo::sm::record_mmap_alloc(1024 * 1024);
ASSERT_EQUALS(mongo::sm::get_mmap_bytes(), 1024 * 1024);
ASSERT_EQUALS(mongo::sm::get_total_bytes(), 1024 * 1024);
mongo::sm::record_mmap_free(1024 * 1024);
ASSERT_EQUALS(mongo::sm::get_mmap_bytes(), 0);
ASSERT_EQUALS(mongo::sm::get_total_bytes(), 0);
}
// mmap bytes count towards the shared total and can trigger OOM (signal_oom is
// a safe no-op in unit-test context where no JSContext is running).
TEST_F(JSCustomAllocatorTest, MmapCountedTowardsLimit) {
constexpr size_t kLimit = 2 * 1024 * 1024; // 2 MB
mongo::sm::reset(kLimit, true);
// A malloc allocation of 0.5 MB should succeed and register in the total.
void* ptr = js_malloc(512 * 1024);
ASSERT_NOT_EQUALS(ptr, nullptr);
// First mmap alloc (1 MB): total is now ~1.5 MB, still under the 2 MB limit.
mongo::sm::record_mmap_alloc(1024 * 1024);
ASSERT_GREATER_THAN_OR_EQUALS(mongo::sm::get_total_bytes(), (1024 * 1024) + (512 * 1024));
// Second mmap alloc (1 MB): pushes total over 2 MB limit. signal_oom() is
// called internally but is a safe no-op here; the counter still increments.
mongo::sm::record_mmap_alloc(1024 * 1024);
ASSERT_GREATER_THAN(mongo::sm::get_total_bytes(), kLimit);
// Clean up.
mongo::sm::record_mmap_free(2 * 1024 * 1024);
js_free(ptr);
ASSERT_EQUALS(mongo::sm::get_mmap_bytes(), 0);
ASSERT_EQUALS(mongo::sm::get_total_bytes(), 0);
}
// malloc_bytes and mmap_bytes are tracked independently; get_total_bytes() is
// their sum.
TEST_F(JSCustomAllocatorTest, MmapAndMallocIndependent) {
mongo::sm::reset(10 * 1024 * 1024, true);
void* ptr = js_malloc(64 * 1024);
ASSERT_NOT_EQUALS(ptr, nullptr);
ASSERT_GREATER_THAN(mongo::sm::get_malloc_bytes(), 0);
ASSERT_EQUALS(mongo::sm::get_mmap_bytes(), 0);
mongo::sm::record_mmap_alloc(128 * 1024);
ASSERT_EQUALS(mongo::sm::get_mmap_bytes(), 128 * 1024);
ASSERT_EQUALS(mongo::sm::get_total_bytes(),
mongo::sm::get_malloc_bytes() + mongo::sm::get_mmap_bytes());
mongo::sm::record_mmap_free(128 * 1024);
js_free(ptr);
ASSERT_EQUALS(mongo::sm::get_malloc_bytes(), 0);
ASSERT_EQUALS(mongo::sm::get_mmap_bytes(), 0);
ASSERT_EQUALS(mongo::sm::get_total_bytes(), 0);
}
// Toggling mmap tracking off via reset() suppresses subsequent record_mmap_alloc calls.
TEST_F(JSCustomAllocatorTest, MmapTrackingToggle) {
// Enable tracking with a non-zero limit so record_mmap_alloc actually counts.
mongo::sm::reset(10 * 1024 * 1024, true);
mongo::sm::record_mmap_alloc(1024);
ASSERT_EQUALS(mongo::sm::get_mmap_bytes(), 1024);
// Free before reset — reset() asserts mmap_bytes == 0 as a production invariant
// (it is only called when a new JS runtime is created, at which point all prior
// allocations must already have been freed by the shutdown GC).
mongo::sm::record_mmap_free(1024);
ASSERT_EQUALS(mongo::sm::get_mmap_bytes(), 0);
// Toggle tracking off; subsequent alloc calls should be ignored.
mongo::sm::reset(0, false);
mongo::sm::record_mmap_alloc(2048);
ASSERT_EQUALS(mongo::sm::get_mmap_bytes(), 0);
}
} // namespace mozjs
} // namespace mongo

View File

@ -387,6 +387,8 @@ void MozJSImplScope::_gcCallback(JSContext* rt,
"phase"_attr = (status == JSGC_BEGIN ? "prologue" : "epilogue"),
"reason"_attr = ExplainGCReason(reason),
"total"_attr = mongo::sm::get_total_bytes(),
"malloc"_attr = mongo::sm::get_malloc_bytes(),
"mmap"_attr = mongo::sm::get_mmap_bytes(),
"limit"_attr = mongo::sm::get_max_bytes(),
"gc total"_attr = js::GetGCHeapUsage(rt),
"gc bytes"_attr = JS_GetGCParameter(rt, JSGC_BYTES),
@ -551,6 +553,15 @@ MozJSImplScope::MozRuntime::MozRuntime(const MozJSScriptEngine* engine,
}
}
MozJSImplScope::MozRuntime::~MozRuntime() {
// Explicitly destroy the context before checking mmap_bytes. This should run JS_DestroyContext
// and the shutdown GC that unmaps all GC chunks. After context destructs all GC chunks should
// be unmapped and mmap_bytes should be 0.
_context.reset();
MOZ_ASSERT(mongo::sm::get_mmap_bytes() == 0,
"Expected all GC memory to be unmapped by MozRuntime shutdown");
}
MozJSImplScope::MozJSImplScope(MozJSScriptEngine* engine, boost::optional<int> jsHeapLimitMB)
: _engine(engine),
_mr(engine, jsHeapLimitMB),

View File

@ -572,6 +572,7 @@ private:
struct MozRuntime {
public:
MozRuntime(const MozJSScriptEngine* engine, boost::optional<int> jsHeapLimitMB);
~MozRuntime();
std::unique_ptr<JSContext, std::function<void(JSContext*)>> _context;
};

View File

@ -70,11 +70,18 @@ namespace sm {
namespace {
/**
* These two variables track the total number of bytes handed out, and the
* These variables track the total number of bytes handed out, and the
* maximum number of bytes we will consider handing out. They are set by
* MozJSImplScope on start up.
*
* IMPORTANT: This tracking is accurate as long as all the allocations for a single scope are run
* from the same thread. We enforce this via js::DisableExtraThreads(). However if this changed and
* mozjs GC background threads were allowed to run, deallocations could be issued from a different
* thread than the thread that made the allocation, causing these counters to become inaccurate on
* the main scope thread.
*/
thread_local size_t malloc_bytes = 0;
thread_local size_t mmap_bytes = 0;
thread_local size_t max_bytes = 0;
// Allow disabling mmap tracking as a fallback in case counted memory goes too high
@ -102,11 +109,15 @@ size_t get_malloc_bytes() {
}
size_t get_mmap_bytes() {
return track_mmap_bytes ? js::gc::GetProfilerMemoryCounts().bytes : 0;
return track_mmap_bytes ? mmap_bytes : 0;
}
void reset(size_t bytes, bool track_mmap) {
malloc_bytes = 0;
MOZ_ASSERT(mmap_bytes == 0, "Expected mmap_bytes to be 0 during runtime initialization");
mmap_bytes = 0;
max_bytes = bytes;
track_mmap_bytes = track_mmap;
}
@ -122,12 +133,18 @@ void signal_oom() {
}
}
void check_oom_on_mmap_allocation(size_t bytes) {
// called after an mmap allocation to check whether an oom signal is required
// if track_mmap_bytes is disabled we fall back to legacy logic and completely bypass this path
/**
* Called after an mmap allocation to update memory bookkeeping and check whether an oom signal is
* required. If track_mmap_bytes is disabled we fall back to legacy logic and completely bypass this
* path
*/
void record_mmap_alloc(size_t bytes) {
mmap_bytes += bytes;
size_t mb = get_max_bytes();
size_t tb = get_total_bytes();
if (track_mmap_bytes && mb > 0 && get_total_bytes() + bytes > mb) {
if (track_mmap_bytes && mb > 0 && tb > mb) {
LOGV2(10920001,
"Out of memory on MozJS mmap allocation",
"bytes"_attr = bytes,
@ -137,6 +154,11 @@ void check_oom_on_mmap_allocation(size_t bytes) {
}
}
void record_mmap_free(size_t bytes) {
MOZ_ASSERT(bytes <= mmap_bytes, "Freed mmap byte count exceeds tracked allocated bytes");
mmap_bytes -= std::min(mmap_bytes, bytes);
}
size_t get_current(void* ptr);
/**

View File

@ -49,7 +49,8 @@ extern MOZ_NORETURN MOZ_COLD JS_PUBLIC_API void JS_Assert(const char* s,
*/
namespace mongo {
namespace sm {
JS_PUBLIC_API void check_oom_on_mmap_allocation(size_t bytes);
JS_PUBLIC_API void record_mmap_alloc(size_t bytes);
JS_PUBLIC_API void record_mmap_free(size_t bytes);
} // namespace sm
} // namespace mongo

View File

@ -1143,13 +1143,13 @@ void RecordMemoryAlloc(size_t bytes) {
MOZ_ASSERT(bytes);
MOZ_ASSERT((bytes % pageSize) == 0);
// MONGODB MODIFICATION: Check whether this allocation took us over a
// configured memory limit. This may trigger OOM in the background, but we
// allow the mozjs logic to proceed normally for now
mongo::sm::check_oom_on_mmap_allocation(bytes);
gMappedMemorySizeBytes += bytes;
gMappedMemoryOperations++;
// MONGODB MODIFICATION: Update memory bookkeeping. This may check whether the
// allocation took us over a configured memory limit and trigger OOM in the
// background. Either way, we allow the allocation to proceed normally for now
mongo::sm::record_mmap_alloc(bytes);
}
void RecordMemoryFree(size_t bytes) {
@ -1159,6 +1159,9 @@ void RecordMemoryFree(size_t bytes) {
gMappedMemorySizeBytes -= bytes;
gMappedMemoryOperations++;
// MONGODB MODIFICATION: Update memory bookkeeping
mongo::sm::record_mmap_free(bytes);
}
JS_PUBLIC_API ProfilerMemoryCounts GetProfilerMemoryCounts() {

View File

@ -98,7 +98,8 @@ void InitLargeAllocLimit() {
// allocator implementation in mongo_sources.
#ifndef JS_USE_CUSTOM_ALLOCATOR
JS_PUBLIC_API void mongo::sm::check_oom_on_mmap_allocation(size_t bytes) {}
JS_PUBLIC_API void mongo::sm::record_mmap_alloc(size_t bytes) {}
JS_PUBLIC_API void mongo::sm::record_mmap_free(size_t bytes) {}
JS_PUBLIC_DATA arena_id_t js::MallocArena;
JS_PUBLIC_DATA arena_id_t js::BackgroundMallocArena;

View File

@ -49,7 +49,8 @@ extern MOZ_NORETURN MOZ_COLD JS_PUBLIC_API void JS_Assert(const char* s,
*/
namespace mongo {
namespace sm {
JS_PUBLIC_API void check_oom_on_mmap_allocation(size_t bytes);
JS_PUBLIC_API void record_mmap_alloc(size_t bytes);
JS_PUBLIC_API void record_mmap_free(size_t bytes);
} // namespace sm
} // namespace mongo

View File

@ -39,7 +39,8 @@ JS_PUBLIC_API void reset(size_t max_bytes, bool track_mmap);
JS_PUBLIC_API size_t get_total_bytes();
JS_PUBLIC_API size_t get_max_bytes();
JS_PUBLIC_API void check_oom_on_mmap_allocation(size_t bytes);
JS_PUBLIC_API void record_mmap_alloc(size_t bytes);
JS_PUBLIC_API void record_mmap_free(size_t bytes);
size_t get_malloc_bytes();
size_t get_mmap_bytes();

View File

@ -10,10 +10,10 @@ NAME=spidermonkey
VERSION="140.9.0esr"
LIB_GIT_BRANCH=spidermonkey-esr140.9-cpp-only
LIB_GIT_REVISION=06ac22a94df3a53a8959a1ef10f8fcda99a1867e
LIB_GIT_REVISION=dd4a007275927572d0b10857d3f11b28550d0095
LIB_GIT_REPO=git@github.com:mongodb-forks/spidermonkey.git
# If a local spidermonkey repo exists, this is much faster than fetching from git:
# LIB_GIT_REPO=/home/ubuntu/spidermonkey/.git
# If a local spidermonkey repo exists, this can be faster than fetching from github:
# LIB_GIT_REPO=file:///home/ubuntu/spidermonkey/.git
DEST_DIR=$(git rev-parse --show-toplevel)/src/third_party/mozjs
@ -23,9 +23,8 @@ LIB_GIT_DIR=$(mktemp -d /tmp/import-spidermonkey.XXXXXX)
trap "rm -rf $LIB_GIT_DIR" EXIT
git clone \
--branch "$LIB_GIT_BRANCH" \
--revision "$LIB_GIT_REVISION" \
--depth 1 \
--single-branch \
"$LIB_GIT_REPO" \
"$LIB_GIT_DIR"