change to fixed size offset table + indexed lookup
Reviewed By: javache Differential Revision: D3281989 fbshipit-source-id: 52db9f441dd46024eafac46ae8d32b26640cad71
This commit is contained in:
parent
516bf7bd94
commit
2f41c56f59
|
@ -9,6 +9,8 @@
|
|||
|
||||
#import "RCTJSCExecutor.h"
|
||||
|
||||
#import <cinttypes>
|
||||
#import <memory>
|
||||
#import <pthread.h>
|
||||
|
||||
#ifdef WITH_FB_JSC_TUNING
|
||||
|
@ -38,10 +40,33 @@ NSString *const RCTJavaScriptContextCreatedNotification = @"RCTJavaScriptContext
|
|||
|
||||
static NSString *const RCTJSCProfilerEnabledDefaultsKey = @"RCTJSCProfilerEnabled";
|
||||
|
||||
typedef struct ModuleData {
|
||||
struct __attribute__((packed)) ModuleData {
|
||||
uint32_t offset;
|
||||
uint32_t length;
|
||||
} ModuleData;
|
||||
uint32_t size;
|
||||
};
|
||||
|
||||
using file_ptr = std::unique_ptr<FILE, decltype(&fclose)>;
|
||||
using memory_ptr = std::unique_ptr<void, decltype(&free)>;
|
||||
using table_ptr = std::unique_ptr<ModuleData[], decltype(&free)>;
|
||||
|
||||
struct RandomAccessBundleData {
|
||||
file_ptr bundle;
|
||||
size_t baseOffset;
|
||||
size_t numTableEntries;
|
||||
table_ptr table;
|
||||
RandomAccessBundleData(): bundle(nullptr, fclose), table(nullptr, free) {}
|
||||
};
|
||||
|
||||
struct RandomAccessBundleStartupCode {
|
||||
memory_ptr code;
|
||||
size_t size;
|
||||
static RandomAccessBundleStartupCode empty() {
|
||||
return RandomAccessBundleStartupCode{memory_ptr(nullptr, free), 0};
|
||||
};
|
||||
bool isEmpty() {
|
||||
return !code;
|
||||
}
|
||||
};
|
||||
|
||||
@interface RCTJavaScriptContext : NSObject <RCTInvalidating>
|
||||
|
||||
|
@ -111,9 +136,8 @@ RCT_NOT_IMPLEMENTED(-(instancetype)init)
|
|||
NSThread *_javaScriptThread;
|
||||
CFMutableDictionaryRef _cookieMap;
|
||||
|
||||
FILE *_bundle;
|
||||
JSStringRef _bundleURL;
|
||||
CFMutableDictionaryRef _jsModules;
|
||||
RandomAccessBundleData _randomAccessBundle;
|
||||
}
|
||||
|
||||
@synthesize valid = _valid;
|
||||
|
@ -471,11 +495,8 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context)
|
|||
waitUntilDone:NO];
|
||||
_context = nil;
|
||||
|
||||
if (_jsModules) {
|
||||
CFRelease(_jsModules);
|
||||
fclose(_bundle);
|
||||
}
|
||||
|
||||
_randomAccessBundle.bundle.reset();
|
||||
_randomAccessBundle.table.reset();
|
||||
if (_cookieMap) {
|
||||
CFRelease(_cookieMap);
|
||||
}
|
||||
|
@ -689,36 +710,37 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context)
|
|||
}), 0, @"js_call,json_call", (@{@"objectName": objectName}))];
|
||||
}
|
||||
|
||||
static int streq(const char *a, const char *b)
|
||||
static bool readRandomAccessModule(const RandomAccessBundleData& bundleData, size_t offset, size_t size, char *data)
|
||||
{
|
||||
return strcmp(a, b) == 0;
|
||||
return fseek(bundleData.bundle.get(), offset + bundleData.baseOffset, SEEK_SET) == 0 &&
|
||||
fread(data, 1, size, bundleData.bundle.get()) == size;
|
||||
}
|
||||
|
||||
static void freeModule(__unused CFAllocatorRef allocator, void *ptr)
|
||||
static void executeRandomAccessModule(RCTJSCExecutor *executor, uint32_t moduleID, size_t offset, size_t size)
|
||||
{
|
||||
free(ptr);
|
||||
}
|
||||
|
||||
static uint32_t readUint32(const char **ptr)
|
||||
{
|
||||
uint32_t data;
|
||||
memcpy(&data, *ptr, sizeof(uint32_t));
|
||||
data = NSSwapLittleIntToHost(data);
|
||||
*ptr += sizeof(uint32_t);
|
||||
return data;
|
||||
}
|
||||
|
||||
static int readBundle(FILE *fd, size_t offset, size_t length, void *ptr)
|
||||
{
|
||||
if (fseek(fd, offset, SEEK_SET) != 0) {
|
||||
return 1;
|
||||
auto data = std::unique_ptr<char[]>(new char[size]);
|
||||
if (!readRandomAccessModule(executor->_randomAccessBundle, offset, size, data.get())) {
|
||||
RCTFatal(RCTErrorWithMessage(@"Error loading RAM module"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (fread(ptr, sizeof(uint8_t), length, fd) != length) {
|
||||
return 1;
|
||||
}
|
||||
static char url[14]; // 10 = maximum decimal digits in a 32bit unsigned int + ".js" + null byte
|
||||
sprintf(url, "%" PRIu32 ".js", moduleID);
|
||||
|
||||
return 0;
|
||||
JSStringRef code = JSStringCreateWithUTF8CString(data.get());
|
||||
JSValueRef jsError = NULL;
|
||||
JSStringRef sourceURL = JSStringCreateWithUTF8CString(url);
|
||||
JSValueRef result = JSEvaluateScript(executor->_context.ctx, code, NULL, sourceURL, 0, &jsError);
|
||||
|
||||
JSStringRelease(code);
|
||||
JSStringRelease(sourceURL);
|
||||
|
||||
if (!result) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
RCTFatal(RCTNSErrorFromJSError(executor->_context.ctx, jsError));
|
||||
[executor invalidate];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)registerNativeRequire
|
||||
|
@ -728,52 +750,79 @@ static int readBundle(FILE *fd, size_t offset, size_t length, void *ptr)
|
|||
RCTPerformanceLoggerSet(RCTPLRAMNativeRequiresSize, 0);
|
||||
|
||||
__weak RCTJSCExecutor *weakSelf = self;
|
||||
[self addSynchronousHookWithName:@"nativeRequire" usingBlock:^(NSString *moduleName) {
|
||||
[self addSynchronousHookWithName:@"nativeRequire" usingBlock:^(NSNumber *moduleID) {
|
||||
RCTJSCExecutor *strongSelf = weakSelf;
|
||||
if (!strongSelf || !moduleName) {
|
||||
if (!strongSelf || !moduleID) {
|
||||
return;
|
||||
}
|
||||
|
||||
RCTPerformanceLoggerAdd(RCTPLRAMNativeRequiresCount, 1);
|
||||
RCTPerformanceLoggerAppendStart(RCTPLRAMNativeRequires);
|
||||
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
|
||||
[@"nativeRequire_" stringByAppendingString:moduleName], nil);
|
||||
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
|
||||
[@"nativeRequire_" stringByAppendingFormat:@"%@", moduleID], nil);
|
||||
|
||||
ModuleData *data = (ModuleData *)CFDictionaryGetValue(strongSelf->_jsModules, moduleName.UTF8String);
|
||||
RCTPerformanceLoggerAdd(RCTPLRAMNativeRequiresSize, data->length);
|
||||
const uint32_t ID = [moduleID unsignedIntValue];
|
||||
|
||||
char bytes[data->length];
|
||||
if (readBundle(strongSelf->_bundle, data->offset, data->length, bytes) != 0) {
|
||||
RCTFatal(RCTErrorWithMessage(@"Error loading RAM module"));
|
||||
return;
|
||||
if (ID < strongSelf->_randomAccessBundle.numTableEntries) {
|
||||
ModuleData *moduleData = &strongSelf->_randomAccessBundle.table[ID];
|
||||
const uint32_t size = NSSwapLittleIntToHost(moduleData->size);
|
||||
|
||||
// sparse entry in the table -- module does not exist or is contained in the startup section
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
RCTPerformanceLoggerAdd(RCTPLRAMNativeRequiresSize, size);
|
||||
executeRandomAccessModule(strongSelf, ID, NSSwapLittleIntToHost(moduleData->offset), size);
|
||||
}
|
||||
JSStringRef code = JSStringCreateWithUTF8CString(bytes);
|
||||
JSValueRef jsError = NULL;
|
||||
JSStringRef sourceURL = JSStringCreateWithUTF8CString([moduleName stringByAppendingPathExtension:@"js"].UTF8String);
|
||||
|
||||
JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, code, NULL, sourceURL, 0, NULL);
|
||||
|
||||
CFDictionaryRemoveValue(strongSelf->_jsModules, moduleName.UTF8String);
|
||||
JSStringRelease(code);
|
||||
JSStringRelease(sourceURL);
|
||||
|
||||
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"js_call", nil);
|
||||
RCTPerformanceLoggerAppendEnd(RCTPLRAMNativeRequires);
|
||||
|
||||
if (!result) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
RCTFatal(RCTNSErrorFromJSError(strongSelf->_context.ctx, jsError));
|
||||
[strongSelf invalidate];
|
||||
});
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
static RandomAccessBundleStartupCode readRAMBundle(file_ptr bundle, RandomAccessBundleData& randomAccessBundle)
|
||||
{
|
||||
// read in magic header, number of entries, and length of the startup section
|
||||
uint32_t header[3];
|
||||
if (fread(&header, 1, sizeof(header), bundle.get()) != sizeof(header)) {
|
||||
return RandomAccessBundleStartupCode::empty();
|
||||
}
|
||||
|
||||
const size_t numTableEntries = NSSwapLittleIntToHost(header[1]);
|
||||
const size_t startupCodeSize = NSSwapLittleIntToHost(header[2]);
|
||||
const size_t tableSize = numTableEntries * sizeof(ModuleData);
|
||||
|
||||
// allocate memory for meta data and lookup table. malloc instead of new to avoid constructor calls
|
||||
table_ptr table(static_cast<ModuleData *>(malloc(tableSize)), free);
|
||||
if (!table) {
|
||||
return RandomAccessBundleStartupCode::empty();
|
||||
}
|
||||
|
||||
// read the lookup table from the file
|
||||
if (fread(table.get(), 1, tableSize, bundle.get()) != tableSize) {
|
||||
return RandomAccessBundleStartupCode::empty();
|
||||
}
|
||||
|
||||
// read the startup code
|
||||
memory_ptr code(malloc(startupCodeSize), free);
|
||||
if (!code || fread(code.get(), 1, startupCodeSize, bundle.get()) != startupCodeSize) {
|
||||
return RandomAccessBundleStartupCode::empty();
|
||||
}
|
||||
|
||||
randomAccessBundle.bundle = std::move(bundle);
|
||||
randomAccessBundle.baseOffset = sizeof(header) + tableSize;
|
||||
randomAccessBundle.numTableEntries = numTableEntries;
|
||||
randomAccessBundle.table = std::move(table);
|
||||
|
||||
return {std::move(code), startupCodeSize};
|
||||
}
|
||||
|
||||
- (NSData *)loadRAMBundle:(NSURL *)sourceURL error:(NSError **)error
|
||||
{
|
||||
RCTPerformanceLoggerStart(RCTPLRAMBundleLoad);
|
||||
_bundle = fopen(sourceURL.path.UTF8String, "r");
|
||||
if (!_bundle) {
|
||||
file_ptr bundle(fopen(sourceURL.path.UTF8String, "r"), fclose);
|
||||
if (!bundle) {
|
||||
if (error) {
|
||||
*error = RCTErrorWithMessage([NSString stringWithFormat:@"Bundle %@ cannot be opened: %d", sourceURL.path, errno]);
|
||||
}
|
||||
|
@ -782,82 +831,18 @@ static int readBundle(FILE *fd, size_t offset, size_t length, void *ptr)
|
|||
|
||||
[self registerNativeRequire];
|
||||
|
||||
// once a module has been loaded free its space from the heap, remove it from the index and release the module name
|
||||
CFDictionaryKeyCallBacks keyCallbacks = { 0, NULL, (CFDictionaryReleaseCallBack)freeModule, NULL, (CFDictionaryEqualCallBack)streq, (CFDictionaryHashCallBack)strlen };
|
||||
CFDictionaryValueCallBacks valueCallbacks = { 0, NULL, (CFDictionaryReleaseCallBack)freeModule, NULL, NULL };
|
||||
_jsModules = CFDictionaryCreateMutable(NULL, 0, &keyCallbacks, &valueCallbacks);
|
||||
|
||||
uint32_t currentOffset = sizeof(uint32_t); // skip magic number
|
||||
|
||||
uint32_t tableLength;
|
||||
if (readBundle(_bundle, currentOffset, sizeof(tableLength), &tableLength) != 0) {
|
||||
if (error) {
|
||||
*error = RCTErrorWithMessage(@"Error loading RAM Bundle");
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
tableLength = NSSwapLittleIntToHost(tableLength);
|
||||
|
||||
currentOffset += sizeof(uint32_t); // skip table length
|
||||
|
||||
// base offset to add to every module's offset to skip the header of the RAM bundle
|
||||
uint32_t baseOffset = 4 + tableLength;
|
||||
|
||||
char tableStart[tableLength];
|
||||
if (readBundle(_bundle, currentOffset, tableLength, tableStart) != 0) {
|
||||
auto startupCode = readRAMBundle(std::move(bundle), _randomAccessBundle);
|
||||
if (startupCode.isEmpty()) {
|
||||
if (error) {
|
||||
*error = RCTErrorWithMessage(@"Error loading RAM Bundle");
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
char *tableCursor = tableStart;
|
||||
char *endOfTable = tableCursor + tableLength;
|
||||
|
||||
while (tableCursor < endOfTable) {
|
||||
uint32_t nameLength = strlen((const char *)tableCursor);
|
||||
char *name = (char *)malloc(nameLength + 1);
|
||||
|
||||
if (!name) {
|
||||
if (error) {
|
||||
*error = RCTErrorWithMessage(@"Error loading RAM Bundle");
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
strcpy(name, tableCursor);
|
||||
|
||||
// the space allocated for each module's metada gets freed when the module is injected into JSC on `nativeRequire`
|
||||
ModuleData *moduleData = (ModuleData *)malloc(sizeof(ModuleData));
|
||||
|
||||
tableCursor += nameLength + 1; // null byte terminator
|
||||
|
||||
moduleData->offset = baseOffset + readUint32((const char **)&tableCursor);
|
||||
moduleData->length = readUint32((const char **)&tableCursor);
|
||||
|
||||
CFDictionarySetValue(_jsModules, name, moduleData);
|
||||
}
|
||||
|
||||
ModuleData *startupData = ((ModuleData *)CFDictionaryGetValue(_jsModules, ""));
|
||||
|
||||
void *startupCode;
|
||||
if (!(startupCode = malloc(startupData->length))) {
|
||||
if (error) {
|
||||
*error = RCTErrorWithMessage(@"Error loading RAM Bundle");
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (readBundle(_bundle, startupData->offset, startupData->length, startupCode) != 0) {
|
||||
if (error) {
|
||||
*error = RCTErrorWithMessage(@"Error loading RAM Bundle");
|
||||
}
|
||||
free(startupCode);
|
||||
return nil;
|
||||
}
|
||||
RCTPerformanceLoggerEnd(RCTPLRAMBundleLoad);
|
||||
RCTPerformanceLoggerSet(RCTPLRAMStartupCodeSize, startupData->length);
|
||||
return [NSData dataWithBytesNoCopy:startupCode length:startupData->length freeWhenDone:YES];
|
||||
RCTPerformanceLoggerSet(RCTPLRAMStartupCodeSize, startupCode.size);
|
||||
return [NSData dataWithBytesNoCopy:startupCode.code.release() length:startupCode.size freeWhenDone:YES];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(setContextName:(nonnull NSString *)name)
|
||||
|
|
|
@ -15,7 +15,7 @@ const writeSourceMap = require('./write-sourcemap');
|
|||
const {joinModules} = require('./util');
|
||||
|
||||
const MAGIC_UNBUNDLE_FILE_HEADER = require('./magic-number');
|
||||
const MAGIC_STARTUP_MODULE_ID = '';
|
||||
const SIZEOF_UINT32 = 4;
|
||||
|
||||
/**
|
||||
* Saves all JS modules of an app as a single file, separated with null bytes.
|
||||
|
@ -77,67 +77,67 @@ function moduleToBuffer(id, code, encoding) {
|
|||
};
|
||||
}
|
||||
|
||||
function uInt32Buffer(n) {
|
||||
const buffer = Buffer(4);
|
||||
buffer.writeUInt32LE(n, 0);
|
||||
return buffer;
|
||||
function entryOffset(n) {
|
||||
// 2: num_entries + startup_code_len
|
||||
// n * 2: each entry consists of two uint32s
|
||||
return (2 + n * 2) * SIZEOF_UINT32;
|
||||
}
|
||||
|
||||
function buildModuleTable(buffers) {
|
||||
function buildModuleTable(startupCode, buffers) {
|
||||
// table format:
|
||||
// - table_length: uint_32 length of all table entries in bytes + the table length itself
|
||||
// - entries: entry...
|
||||
// - num_entries: uint_32 number of entries
|
||||
// - startup_code_len: uint_32 length of the startup section
|
||||
// - entries: entry...
|
||||
//
|
||||
// entry:
|
||||
// - module_id: NUL terminated utf8 string
|
||||
// - module_offset: uint_32 offset into the module string
|
||||
// - module_length: uint_32 length in bytes of the module
|
||||
// - module_offset: uint_32 offset into the modules blob
|
||||
// - module_length: uint_32 length of the module code in bytes
|
||||
|
||||
const numBuffers = buffers.length;
|
||||
const maxId = buffers.reduce((max, {id}) => Math.max(max, id), 0);
|
||||
const numEntries = maxId + 1;
|
||||
const table = new Buffer(entryOffset(numEntries)).fill(0);
|
||||
|
||||
const tableLengthBuffer = uInt32Buffer(0);
|
||||
let tableLength = 4; // the table length itself, 4 == tableLengthBuffer.length
|
||||
let currentOffset = 0;
|
||||
// num_entries
|
||||
table.writeUInt32LE(numEntries, 0);
|
||||
|
||||
const offsetTable = [tableLengthBuffer];
|
||||
for (let i = 0; i < numBuffers; i++) {
|
||||
const {id, buffer: {length}} = buffers[i];
|
||||
// startup_code_len
|
||||
table.writeUInt32LE(startupCode.length, SIZEOF_UINT32);
|
||||
|
||||
const entry = Buffer.concat([
|
||||
Buffer(i === 0 ? MAGIC_STARTUP_MODULE_ID : id, 'utf8'),
|
||||
nullByteBuffer,
|
||||
uInt32Buffer(currentOffset),
|
||||
uInt32Buffer(length),
|
||||
]);
|
||||
// entries
|
||||
let codeOffset = startupCode.length;
|
||||
buffers.forEach(({id, buffer}) => {
|
||||
const offset = entryOffset(id);
|
||||
// module_offset
|
||||
table.writeUInt32LE(codeOffset, offset);
|
||||
// module_length
|
||||
table.writeUInt32LE(buffer.length, offset + SIZEOF_UINT32);
|
||||
codeOffset += buffer.length;
|
||||
});
|
||||
|
||||
currentOffset += length;
|
||||
tableLength += entry.length;
|
||||
offsetTable.push(entry);
|
||||
}
|
||||
|
||||
tableLengthBuffer.writeUInt32LE(tableLength, 0);
|
||||
return Buffer.concat(offsetTable);
|
||||
return table;
|
||||
}
|
||||
|
||||
function buildModuleBuffers(startupCode, modules, encoding) {
|
||||
return (
|
||||
[moduleToBuffer('', startupCode, encoding, true)].concat(
|
||||
modules.map(module =>
|
||||
moduleToBuffer(
|
||||
String(module.id),
|
||||
module.code,
|
||||
encoding,
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
function buildModuleBuffers(modules, encoding) {
|
||||
return modules.map(
|
||||
module => moduleToBuffer(module.id, module.code, encoding));
|
||||
}
|
||||
|
||||
function buildTableAndContents(startupCode, modules, encoding) {
|
||||
const buffers = buildModuleBuffers(startupCode, modules, encoding);
|
||||
const table = buildModuleTable(buffers, encoding);
|
||||
// file contents layout:
|
||||
// - magic number char[4] 0xE5 0xD1 0x0B 0xFB (0xFB0BD1E5 uint32 LE)
|
||||
// - offset table table see `buildModuleTables`
|
||||
// - code blob char[] null-terminated code strings, starting with
|
||||
// the startup code
|
||||
|
||||
return [fileHeader, table].concat(buffers.map(({buffer}) => buffer));
|
||||
const startupCodeBuffer = Buffer(startupCode, encoding);
|
||||
const moduleBuffers = buildModuleBuffers(modules, encoding);
|
||||
const table = buildModuleTable(startupCodeBuffer, moduleBuffers);
|
||||
|
||||
return [
|
||||
fileHeader,
|
||||
table,
|
||||
startupCodeBuffer
|
||||
].concat(moduleBuffers.map(({buffer}) => buffer));
|
||||
}
|
||||
|
||||
module.exports = saveAsIndexedFile;
|
||||
|
|
Loading…
Reference in New Issue