Skip to content

Commit 15c1eec

Browse files
committed
Remove retain cycle from JSScript and also don't keep the cache file descriptor open so many JSScripts can be cached in a loop
https://bugs.webkit.org/show_bug.cgi?id=195782 <rdar://problem/48880625> Reviewed by Michael Saboff. This patch fixes two issues with JSScript API: 1. There was a retain cycle causing us to never destroy a JSScript once it created a JSSourceCode. The reason for this is that JSScript had a Strong<JSSourceCode> field. And JSSourceCode transitively had RetainPtr<JSScript>. This patch fixes this issue by making the "jsSourceCode" accessor return a transient object. 2. r242585 made it so that JSScript would keep the cache file descriptor open (and locked) for the duration of the lifetime of the JSScript itself. Our anticipation here is that it would make implementing iterative cache updates easier. However, this made using the API super limiting in other ways. For example, if a program had a loop that cached 3000 different JSScripts, it's likely that such a program would exhaust the open file count limit. This patch reverts to the behavior prior to r242585 where we just keep open the file descriptor while we read or write it. * API/JSAPIGlobalObject.mm: (JSC::JSAPIGlobalObject::moduleLoaderFetch): * API/JSContext.mm: (-[JSContext evaluateJSScript:]): * API/JSScript.mm: (-[JSScript dealloc]): (-[JSScript readCache]): (-[JSScript init]): (-[JSScript sourceCode]): (-[JSScript jsSourceCode]): (-[JSScript writeCache:]): (-[JSScript forceRecreateJSSourceCode]): Deleted. * API/JSScriptInternal.h: * API/tests/testapi.mm: (testCanCacheManyFilesWithTheSameVM): (testObjectiveCAPI): (testCacheFileIsExclusive): Deleted. Canonical link: https://commits.webkit.org/210067@main git-svn-id: https://svn.webkit.org/repository/webkit/trunk@242980 268f45cc-cd09-0410-ab3c-d52691b4dbfc
1 parent 4118bb2 commit 15c1eec

6 files changed

Lines changed: 134 additions & 71 deletions

File tree

‎Source/JavaScriptCore/API/JSAPIGlobalObject.mm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@
204204
return rejectPromise(makeString("The same JSScript was provided for two different identifiers, previously: ", oldModuleKey, " and now: ", moduleKey.string()));
205205
} else {
206206
[jsScript setSourceURL:[NSURL URLWithString:static_cast<NSString *>(moduleKey.string())]];
207-
source = [jsScript forceRecreateJSSourceCode];
207+
source = [jsScript jsSourceCode];
208208
}
209209

210210
args.append(source);

‎Source/JavaScriptCore/API/JSContext.mm‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ - (JSValue *)evaluateJSScript:(JSScript *)script
124124

125125
if (script.type == kJSScriptTypeProgram) {
126126
JSValueRef exceptionValue = nullptr;
127-
JSValueRef result = JSEvaluateScriptInternal(locker, exec, m_context, nullptr, [script jsSourceCode]->sourceCode(), &exceptionValue);
127+
JSC::SourceCode sourceCode = [script sourceCode];
128+
JSValueRef result = JSEvaluateScriptInternal(locker, exec, m_context, nullptr, sourceCode, &exceptionValue);
128129

129130
if (exceptionValue)
130131
return [self valueFromNotifyException:exceptionValue];

‎Source/JavaScriptCore/API/JSScript.mm‎

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
#import "Symbol.h"
4040
#include <sys/stat.h>
4141
#include <wtf/FileSystem.h>
42+
#include <wtf/Scope.h>
4243

4344
#if JSC_OBJC_API_ENABLED
4445

@@ -50,8 +51,6 @@ @implementation JSScript {
5051
RetainPtr<NSURL> m_sourceURL;
5152
RetainPtr<NSURL> m_cachePath;
5253
JSC::CachedBytecode m_cachedBytecode;
53-
JSC::Strong<JSC::JSSourceCode> m_jsSourceCode;
54-
int m_cacheFileDescriptor;
5554
}
5655

5756
+ (instancetype)scriptWithSource:(NSString *)source inVirtualMachine:(JSVirtualMachine *)vm
@@ -175,9 +174,6 @@ - (void)dealloc
175174
if (m_cachedBytecode.size() && !m_cachedBytecode.owned())
176175
munmap(const_cast<void*>(m_cachedBytecode.data()), m_cachedBytecode.size());
177176

178-
if (m_cacheFileDescriptor != -1)
179-
close(m_cacheFileDescriptor);
180-
181177
[super dealloc];
182178
}
183179

@@ -186,27 +182,30 @@ - (void)readCache
186182
if (!m_cachePath)
187183
return;
188184

189-
m_cacheFileDescriptor = open([m_cachePath path].UTF8String, O_CREAT | O_RDWR | O_EXLOCK | O_NONBLOCK, 0666);
190-
if (m_cacheFileDescriptor == -1)
185+
int fd = open([m_cachePath path].UTF8String, O_RDONLY | O_EXLOCK | O_NONBLOCK, 0666);
186+
if (fd == -1)
191187
return;
188+
auto closeFD = makeScopeExit([&] {
189+
close(fd);
190+
});
192191

193192
struct stat sb;
194-
int res = fstat(m_cacheFileDescriptor, &sb);
193+
int res = fstat(fd, &sb);
195194
size_t size = static_cast<size_t>(sb.st_size);
196195
if (res || !size)
197196
return;
198197

199-
void* buffer = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, m_cacheFileDescriptor, 0);
198+
void* buffer = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
200199

201200
JSC::CachedBytecode cachedBytecode { buffer, size };
202201

203202
JSC::VM& vm = m_virtualMachine.vm;
204-
const JSC::SourceCode& sourceCode = [self jsSourceCode]->sourceCode();
203+
JSC::SourceCode sourceCode = [self sourceCode];
205204
JSC::SourceCodeKey key = m_type == kJSScriptTypeProgram ? sourceCodeKeyForSerializedProgram(vm, sourceCode) : sourceCodeKeyForSerializedModule(vm, sourceCode);
206205
if (isCachedBytecodeStillValid(vm, cachedBytecode, key, m_type == kJSScriptTypeProgram ? JSC::SourceCodeType::ProgramType : JSC::SourceCodeType::ModuleType))
207206
m_cachedBytecode = WTFMove(cachedBytecode);
208207
else
209-
ftruncate(m_cacheFileDescriptor, 0);
208+
ftruncate(fd, 0);
210209
}
211210

212211
- (BOOL)cacheBytecodeWithError:(NSError **)error
@@ -241,7 +240,6 @@ - (instancetype)init
241240
if (!self)
242241
return nil;
243242

244-
m_cacheFileDescriptor = -1;
245243
return self;
246244
}
247245

@@ -260,12 +258,25 @@ - (unsigned)hash
260258
return &m_cachedBytecode;
261259
}
262260

263-
- (JSC::JSSourceCode*)jsSourceCode
261+
- (JSC::SourceCode)sourceCode
264262
{
265-
if (m_jsSourceCode)
266-
return m_jsSourceCode.get();
263+
JSC::VM& vm = m_virtualMachine.vm;
264+
JSC::JSLockHolder locker(vm);
265+
266+
TextPosition startPosition { };
267+
String url = String { [[self sourceURL] absoluteString] };
268+
auto type = m_type == kJSScriptTypeModule ? JSC::SourceProviderSourceType::Module : JSC::SourceProviderSourceType::Program;
269+
Ref<JSScriptSourceProvider> sourceProvider = JSScriptSourceProvider::create(self, JSC::SourceOrigin(url), URL({ }, url), startPosition, type);
270+
JSC::SourceCode sourceCode(WTFMove(sourceProvider), startPosition.m_line.oneBasedInt(), startPosition.m_column.oneBasedInt());
271+
return sourceCode;
272+
}
267273

268-
return [self forceRecreateJSSourceCode];
274+
- (JSC::JSSourceCode*)jsSourceCode
275+
{
276+
JSC::VM& vm = m_virtualMachine.vm;
277+
JSC::JSLockHolder locker(vm);
278+
JSC::JSSourceCode* jsSourceCode = JSC::JSSourceCode::create(vm, [self sourceCode]);
279+
return jsSourceCode;
269280
}
270281

271282
- (BOOL)writeCache:(String&)error
@@ -275,21 +286,28 @@ - (BOOL)writeCache:(String&)error
275286
return NO;
276287
}
277288

278-
if (m_cacheFileDescriptor == -1) {
279-
if (!m_cachePath)
280-
error = "No cache was path provided during construction of this JSScript."_s;
281-
else
282-
error = "Could not lock the bytecode cache file. It's likely another VM or process is already using it."_s;
289+
if (!m_cachePath) {
290+
error = "No cache path was provided during construction of this JSScript."_s;
283291
return NO;
284292
}
285293

294+
int fd = open([m_cachePath path].UTF8String, O_CREAT | O_RDWR | O_EXLOCK | O_NONBLOCK, 0666);
295+
if (fd == -1) {
296+
error = makeString("Could not open or lock the bytecode cache file. It's likely another VM or process is already using it. Error: ", strerror(errno));
297+
return NO;
298+
}
299+
auto closeFD = makeScopeExit([&] {
300+
close(fd);
301+
});
302+
286303
JSC::ParserError parserError;
304+
JSC::SourceCode sourceCode = [self sourceCode];
287305
switch (m_type) {
288306
case kJSScriptTypeModule:
289-
m_cachedBytecode = JSC::generateModuleBytecode(m_virtualMachine.vm, [self jsSourceCode]->sourceCode(), parserError);
307+
m_cachedBytecode = JSC::generateModuleBytecode(m_virtualMachine.vm, sourceCode, parserError);
290308
break;
291309
case kJSScriptTypeProgram:
292-
m_cachedBytecode = JSC::generateProgramBytecode(m_virtualMachine.vm, [self jsSourceCode]->sourceCode(), parserError);
310+
m_cachedBytecode = JSC::generateProgramBytecode(m_virtualMachine.vm, sourceCode, parserError);
293311
break;
294312
}
295313

@@ -299,14 +317,14 @@ - (BOOL)writeCache:(String&)error
299317
return NO;
300318
}
301319

302-
ssize_t bytesWritten = write(m_cacheFileDescriptor, m_cachedBytecode.data(), m_cachedBytecode.size());
320+
ssize_t bytesWritten = write(fd, m_cachedBytecode.data(), m_cachedBytecode.size());
303321
if (bytesWritten == -1) {
304322
error = makeString("Could not write cache file to disk: ", strerror(errno));
305323
return NO;
306324
}
307325

308326
if (static_cast<size_t>(bytesWritten) != m_cachedBytecode.size()) {
309-
ftruncate(m_cacheFileDescriptor, 0);
327+
ftruncate(fd, 0);
310328
error = makeString("Could not write the full cache file to disk. Only wrote ", String::number(bytesWritten), " of the expected ", String::number(m_cachedBytecode.size()), " bytes.");
311329
return NO;
312330
}
@@ -319,22 +337,6 @@ - (void)setSourceURL:(NSURL *)url
319337
m_sourceURL = url;
320338
}
321339

322-
- (JSC::JSSourceCode*)forceRecreateJSSourceCode
323-
{
324-
JSC::VM& vm = m_virtualMachine.vm;
325-
JSC::JSLockHolder locker(vm);
326-
327-
TextPosition startPosition { };
328-
329-
String url = String { [[self sourceURL] absoluteString] };
330-
auto type = m_type == kJSScriptTypeModule ? JSC::SourceProviderSourceType::Module : JSC::SourceProviderSourceType::Program;
331-
Ref<JSScriptSourceProvider> sourceProvider = JSScriptSourceProvider::create(self, JSC::SourceOrigin(url), URL({ }, url), startPosition, type);
332-
JSC::SourceCode sourceCode(WTFMove(sourceProvider), startPosition.m_line.oneBasedInt(), startPosition.m_column.oneBasedInt());
333-
JSC::JSSourceCode* jsSourceCode = JSC::JSSourceCode::create(vm, WTFMove(sourceCode));
334-
m_jsSourceCode.set(vm, jsSourceCode);
335-
return jsSourceCode;
336-
}
337-
338340
@end
339341

340342
#endif

‎Source/JavaScriptCore/API/JSScriptInternal.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ class String;
4949
- (const WTF::String&)source;
5050
- (nullable const JSC::CachedBytecode*)cachedBytecode;
5151
- (JSC::JSSourceCode*)jsSourceCode;
52-
// FIXME: Remove this once we require sourceURL upon creation: https://bugs.webkit.org/show_bug.cgi?id=194909
53-
- (JSC::JSSourceCode*)forceRecreateJSSourceCode;
52+
- (JSC::SourceCode)sourceCode;
5453
- (BOOL)writeCache:(String&)error;
54+
// FIXME: Remove this once we require sourceURL upon creation: https://bugs.webkit.org/show_bug.cgi?id=194909
5555
- (void)setSourceURL:(NSURL *)url;
5656

5757
@end

‎Source/JavaScriptCore/API/tests/testapi.mm‎

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2159,31 +2159,6 @@ static void testProgramJSScriptException()
21592159
}
21602160
}
21612161

2162-
static void testCacheFileIsExclusive()
2163-
{
2164-
NSURL* cachePath = tempFile(@"foo.program.cache");
2165-
2166-
@autoreleasepool {
2167-
NSString *source = @"function foo() { return 42; } foo();";
2168-
NSURL* sourceURL = [NSURL URLWithString:@"my-path"];
2169-
JSVirtualMachine *vm = [[JSVirtualMachine alloc] init];
2170-
2171-
JSScript *script1 = [JSScript scriptOfType:kJSScriptTypeProgram withSource:source andSourceURL:sourceURL andBytecodeCache:cachePath inVirtualMachine:vm error:nil];
2172-
RELEASE_ASSERT(script1);
2173-
checkResult(@"Should be able to cache the first file", [script1 cacheBytecodeWithError:nil]);
2174-
2175-
JSScript *script2 = [JSScript scriptOfType:kJSScriptTypeProgram withSource:source andSourceURL:sourceURL andBytecodeCache:cachePath inVirtualMachine:vm error:nil];
2176-
RELEASE_ASSERT(script2);
2177-
NSError* error = nil;
2178-
checkResult(@"Should NOT be able to cache the second file", ![script2 cacheBytecodeWithError:&error]);
2179-
checkResult(@"Should NOT be able to cache the second file has the correct error message", [[error description] containsString:@"Could not lock the bytecode cache file. It's likely another VM or process is already using it"]);
2180-
}
2181-
2182-
NSFileManager* fileManager = [NSFileManager defaultManager];
2183-
BOOL removedAll = [fileManager removeItemAtURL:cachePath error:nil];
2184-
checkResult(@"Successfully removed cache file", removedAll);
2185-
}
2186-
21872162
static void testCacheFileFailsWhenItsAlreadyCached()
21882163
{
21892164
NSURL* cachePath = tempFile(@"foo.program.cache");
@@ -2220,6 +2195,48 @@ static void testCacheFileFailsWhenItsAlreadyCached()
22202195
checkResult(@"Successfully removed cache file", removedAll);
22212196
}
22222197

2198+
static void testCanCacheManyFilesWithTheSameVM()
2199+
{
2200+
NSMutableArray *cachePaths = [[NSMutableArray alloc] init];
2201+
NSMutableArray *scripts = [[NSMutableArray alloc] init];
2202+
2203+
for (unsigned i = 0; i < 10000; ++i)
2204+
[cachePaths addObject:tempFile([NSString stringWithFormat:@"cache-%d.cache", i])];
2205+
2206+
JSVirtualMachine *vm = [[JSVirtualMachine alloc] init];
2207+
bool cachedAll = true;
2208+
for (NSURL *path : cachePaths) {
2209+
@autoreleasepool {
2210+
NSURL *sourceURL = [NSURL URLWithString:@"id"];
2211+
NSString *source = @"function foo() { return 42; } foo();";
2212+
JSScript *script = [JSScript scriptOfType:kJSScriptTypeProgram withSource:source andSourceURL:sourceURL andBytecodeCache:path inVirtualMachine:vm error:nil];
2213+
RELEASE_ASSERT(script);
2214+
2215+
[scripts addObject:script];
2216+
cachedAll &= [script cacheBytecodeWithError:nil];
2217+
}
2218+
}
2219+
checkResult(@"Cached all 10000 scripts", cachedAll);
2220+
2221+
JSContext *context = [[JSContext alloc] init];
2222+
bool all42 = true;
2223+
for (JSScript *script : scripts) {
2224+
@autoreleasepool {
2225+
JSValue *result = [context evaluateJSScript:script];
2226+
RELEASE_ASSERT(result);
2227+
all42 &= [result isNumber] && [result toInt32] == 42;
2228+
}
2229+
}
2230+
checkResult(@"All scripts returned 42", all42);
2231+
2232+
NSFileManager* fileManager = [NSFileManager defaultManager];
2233+
bool removedAll = true;
2234+
for (NSURL *path : cachePaths)
2235+
removedAll &= [fileManager removeItemAtURL:path error:nil];
2236+
2237+
checkResult(@"Removed all cache files", removedAll);
2238+
}
2239+
22232240
@interface JSContextFileLoaderDelegate : JSContext <JSModuleLoaderDelegate>
22242241

22252242
+ (instancetype)newContext;
@@ -2427,8 +2444,8 @@ void testObjectiveCAPI(const char* filter)
24272444
RUN(testBytecodeCacheWithSameCacheFileAndDifferentScript(false));
24282445
RUN(testBytecodeCacheWithSameCacheFileAndDifferentScript(true));
24292446
RUN(testProgramJSScriptException());
2430-
RUN(testCacheFileIsExclusive());
24312447
RUN(testCacheFileFailsWhenItsAlreadyCached());
2448+
RUN(testCanCacheManyFilesWithTheSameVM());
24322449

24332450
RUN(testLoaderRejectsNilScriptURL());
24342451
RUN(testLoaderRejectsFailedFetch());

‎Source/JavaScriptCore/ChangeLog‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,46 @@
1+
2019-03-14 Saam barati <sbarati@apple.com>
2+
3+
Remove retain cycle from JSScript and also don't keep the cache file descriptor open so many JSScripts can be cached in a loop
4+
https://bugs.webkit.org/show_bug.cgi?id=195782
5+
<rdar://problem/48880625>
6+
7+
Reviewed by Michael Saboff.
8+
9+
This patch fixes two issues with JSScript API:
10+
11+
1. There was a retain cycle causing us to never destroy a JSScript once it
12+
created a JSSourceCode. The reason for this is that JSScript had a
13+
Strong<JSSourceCode> field. And JSSourceCode transitively had RetainPtr<JSScript>.
14+
15+
This patch fixes this issue by making the "jsSourceCode" accessor return a transient object.
16+
17+
2. r242585 made it so that JSScript would keep the cache file descriptor open
18+
(and locked) for the duration of the lifetime of the JSScript itself. Our
19+
anticipation here is that it would make implementing iterative cache updates
20+
easier. However, this made using the API super limiting in other ways. For
21+
example, if a program had a loop that cached 3000 different JSScripts, it's
22+
likely that such a program would exhaust the open file count limit. This patch
23+
reverts to the behavior prior to r242585 where we just keep open the file descriptor
24+
while we read or write it.
25+
26+
* API/JSAPIGlobalObject.mm:
27+
(JSC::JSAPIGlobalObject::moduleLoaderFetch):
28+
* API/JSContext.mm:
29+
(-[JSContext evaluateJSScript:]):
30+
* API/JSScript.mm:
31+
(-[JSScript dealloc]):
32+
(-[JSScript readCache]):
33+
(-[JSScript init]):
34+
(-[JSScript sourceCode]):
35+
(-[JSScript jsSourceCode]):
36+
(-[JSScript writeCache:]):
37+
(-[JSScript forceRecreateJSSourceCode]): Deleted.
38+
* API/JSScriptInternal.h:
39+
* API/tests/testapi.mm:
40+
(testCanCacheManyFilesWithTheSameVM):
41+
(testObjectiveCAPI):
42+
(testCacheFileIsExclusive): Deleted.
43+
144
2019-03-14 Michael Saboff <msaboff@apple.com>
245

346
ASSERTION FAILED: regexp->isValid() or ASSERTION FAILED: !isCompilationThread()

0 commit comments

Comments
 (0)