WebKit Bugzilla
Attachment 360040 Details for
Bug 193401
: Add API to generate and consume cached bytecode
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Requests
|
Help
|
New Account
|
Log In
Remember
[x]
|
Forgot Password
Login:
[x]
[patch]
Patch
bug-193401-20190124233712.patch (text/plain), 39.69 KB, created by
Tadeu Zagallo
on 2019-01-24 14:37:30 PST
(
hide
)
Description:
Patch
Filename:
MIME Type:
Creator:
Tadeu Zagallo
Created:
2019-01-24 14:37:30 PST
Size:
39.69 KB
patch
obsolete
>Subversion Revision: 240343 >diff --git a/Source/JavaScriptCore/ChangeLog b/Source/JavaScriptCore/ChangeLog >index a514c8fddd34f5ca4a729fd42814a627e14109a3..d26c999190e037d68515b9c85236eff48615a783 100644 >--- a/Source/JavaScriptCore/ChangeLog >+++ b/Source/JavaScriptCore/ChangeLog >@@ -1,3 +1,67 @@ >+2019-01-24 Tadeu Zagallo <tzagallo@apple.com> >+ >+ Add API to generate and consume cached bytecode >+ https://bugs.webkit.org/show_bug.cgi?id=193401 >+ <rdar://problem/47514099> >+ >+ Reviewed by NOBODY (OOPS!). >+ >+ Add the `generateBytecode` and `generateModuleBytecode` functions to >+ generate serialized bytecode for a given `SourceCode`. These functions >+ will eagerly generate code for all the nested functions. >+ >+ Additionally, update the API methods in JSScript to generate and use the >+ bytecode when the bytecodeCache path is provided. >+ >+ * API/JSAPIGlobalObject.mm: >+ (JSC::JSAPIGlobalObject::moduleLoaderFetch): >+ * API/JSContext.mm: >+ * API/JSScript.mm: >+ (+[JSScript scriptWithSource:inVirtualMachine:]): >+ (+[JSScript scriptFromASCIIFile:inVirtualMachine:withCodeSigning:andBytecodeCache:]): >+ (-[JSScript dealloc]): >+ (-[JSScript readCache]): >+ (-[JSScript writeCache]): >+ (-[JSScript jsSourceCode:]): >+ * API/JSScriptInternal.h: >+ * API/JSVirtualMachine.mm: >+ (-[JSVirtualMachine vm]): >+ * API/JSVirtualMachineInternal.h: >+ * API/tests/testapi.mm: >+ (testBytecodeCache): >+ (-[JSContextFileLoaderDelegate context:fetchModuleForIdentifier:withResolveHandler:andRejectHandler:]): >+ (testObjectiveCAPI): >+ * bytecode/UnlinkedFunctionExecutable.cpp: >+ (JSC::UnlinkedFunctionExecutable::unlinkedCodeBlockFor): >+ * bytecode/UnlinkedFunctionExecutable.h: >+ * parser/SourceCodeKey.h: >+ (JSC::SourceCodeKey::source const): >+ * parser/SourceProvider.h: >+ (JSC::CachedBytecode::CachedBytecode): >+ (JSC::CachedBytecode::data const): >+ (JSC::CachedBytecode::size const): >+ (JSC::CachedBytecode::~CachedBytecode): >+ (JSC::SourceProvider::cachedBytecode const): >+ (JSC::CachedBytecodeSourceProvider::create): >+ (JSC::CachedBytecodeSourceProvider::CachedBytecodeSourceProvider): >+ * parser/UnlinkedSourceCode.h: >+ (JSC::UnlinkedSourceCode::provider const): >+ * runtime/CodeCache.cpp: >+ (JSC::generateUnlinkedCodeBlockForFunctions): >+ (JSC::writeCodeBlock): >+ (JSC::serializeBytecode): >+ * runtime/CodeCache.h: >+ (JSC::CodeCacheMap::fetchFromDiskImpl): >+ (JSC::CodeCacheMap::findCacheAndUpdateAge): >+ (JSC::generateUnlinkedCodeBlockImpl): >+ (JSC::generateUnlinkedCodeBlock): >+ * runtime/Completion.cpp: >+ (JSC::generateBytecode): >+ (JSC::generateModuleBytecode): >+ * runtime/Completion.h: >+ * runtime/Options.cpp: >+ (JSC::recomputeDependentOptions): >+ > 2019-01-23 David Kilzer <ddkilzer@apple.com> > > [JSC] Duplicate global variables: JSC::opcodeLengths >diff --git a/Source/JavaScriptCore/API/JSAPIGlobalObject.mm b/Source/JavaScriptCore/API/JSAPIGlobalObject.mm >index 3ca4ab9251f71a55ed2484ac1176c3906e992cc5..d5c2365d0847148c80f8992bc044352f31bcbf0e 100644 >--- a/Source/JavaScriptCore/API/JSAPIGlobalObject.mm >+++ b/Source/JavaScriptCore/API/JSAPIGlobalObject.mm >@@ -164,7 +164,6 @@ JSInternalPromise* JSAPIGlobalObject::moduleLoaderFetch(JSGlobalObject* globalOb > auto deferredPromise = Strong<JSInternalPromiseDeferred>(vm, deferred); > auto strongKey = Strong<JSString>(vm, jsSecureCast<JSString*>(vm, key)); > auto* resolve = JSNativeStdFunction::create(vm, globalObject, 1, "resolve", [=] (ExecState* exec) { >- VM& vm = exec->vm(); > // This captures the globalObject but that's ok because our structure keeps it alive anyway. > JSContext *context = [JSContext contextWithJSGlobalContextRef:toGlobalRef(globalObject->globalExec())]; > id script = valueToObject(context, toRef(exec, exec->argument(0))); >@@ -176,8 +175,7 @@ JSInternalPromise* JSAPIGlobalObject::moduleLoaderFetch(JSGlobalObject* globalOb > return encodedJSUndefined(); > } > >- const String& source = getJSScriptSourceCode(static_cast<JSScript *>(script)); >- args.append(JSSourceCode::create(vm, makeSource(source, SourceOrigin(moduleKey.string()), URL({ }, moduleKey.string()), TextPosition(), JSC::SourceProviderSourceType::Module))); >+ args.append([static_cast<JSScript *>(script) jsSourceCode:moduleKey]); > call(exec, deferredPromise->JSPromiseDeferred::resolve(), args, "This should never be seen..."); > return encodedJSUndefined(); > }); >diff --git a/Source/JavaScriptCore/API/JSContext.mm b/Source/JavaScriptCore/API/JSContext.mm >index 3c776d4d900ee1733362ac29f624f444d545e7b4..723b1bbdb6884265a626b6cada9acfb27b9f0b4f 100644 >--- a/Source/JavaScriptCore/API/JSContext.mm >+++ b/Source/JavaScriptCore/API/JSContext.mm >@@ -34,7 +34,6 @@ > #import "JSGlobalObject.h" > #import "JSInternalPromise.h" > #import "JSModuleLoader.h" >-#import "JSScriptInternal.h" > #import "JSValueInternal.h" > #import "JSVirtualMachineInternal.h" > #import "JSWrapperMap.h" >diff --git a/Source/JavaScriptCore/API/JSScript.mm b/Source/JavaScriptCore/API/JSScript.mm >index f7e939af098bf5d0f53ed1e8b32d75a4d823fd7f..8b5ef2e6d3f3c254b8b58d1a948b04db786cd88e 100644 >--- a/Source/JavaScriptCore/API/JSScript.mm >+++ b/Source/JavaScriptCore/API/JSScript.mm >@@ -27,21 +27,31 @@ > #import "JSScriptInternal.h" > > #import "APICast.h" >+#import "Identifier.h" > #import "JSContextInternal.h" >+#import "JSSourceCode.h" > #import "JSValuePrivate.h" >+#import "JSVirtualMachineInternal.h" >+#import "ParserError.h" > #import "Symbol.h" >+#include <sys/stat.h> > > #if JSC_OBJC_API_ENABLED > > @implementation JSScript { >+ __weak JSVirtualMachine* m_virtualMachine; > String m_source; >+ NSURL* m_cachePath; >+ JSC::CachedBytecode m_cachedBytecode; >+ JSC::Strong<JSC::JSSourceCode> m_jsSourceCode; >+ UniquedStringImpl* m_moduleKey; > } > > + (instancetype)scriptWithSource:(NSString *)source inVirtualMachine:(JSVirtualMachine *)vm > { >- UNUSED_PARAM(vm); >- JSScript *result = [[JSScript alloc] init]; >+ JSScript *result = [[[JSScript alloc] init] autorelease]; > result->m_source = source; >+ result->m_virtualMachine = vm; > return result; > } > >@@ -81,9 +91,6 @@ + (instancetype)scriptFromASCIIFile:(NSURL *)filePath inVirtualMachine:(JSVirtua > { > // FIXME: This should check codeSigning. > UNUSED_PARAM(codeSigningPath); >- // FIXME: This should actually cache bytecode. >- UNUSED_PARAM(cachePath); >- UNUSED_PARAM(vm); > URL filePathURL([filePath absoluteURL]); > if (!filePathURL.isLocalFile()) > return nil; >@@ -95,8 +102,11 @@ + (instancetype)scriptFromASCIIFile:(NSURL *)filePath inVirtualMachine:(JSVirtua > if (!charactersAreAllASCII(buffer.data(), buffer.size())) > return nil; > >- JSScript *result = [[JSScript alloc] init]; >+ JSScript *result = [[[JSScript alloc] init] autorelease]; >+ result->m_virtualMachine = vm; > result->m_source = String::fromUTF8WithLatin1Fallback(buffer.data(), buffer.size()); >+ result->m_cachePath = cachePath; >+ [result readCache]; > return result; > } > >@@ -105,7 +115,80 @@ + (instancetype)scriptFromUTF8File:(NSURL *)filePath inVirtualMachine:(JSVirtual > return [JSScript scriptFromASCIIFile:filePath inVirtualMachine:vm withCodeSigning:codeSigningPath andBytecodeCache:cachePath]; > } > >-const String& getJSScriptSourceCode(JSScript *module) { return module->m_source; } >+- (void)dealloc >+{ >+ if (m_cachedBytecode.size()) >+ munmap(const_cast<void*>(m_cachedBytecode.data()), m_cachedBytecode.size()); >+ [super dealloc]; >+} >+ >+- (void)readCache >+{ >+ if (!m_cachePath) >+ return; >+ >+ int fd = open(m_cachePath.path.UTF8String, O_RDONLY); >+ if (fd == -1) >+ return; >+ >+ int rc = flock(fd, LOCK_SH | LOCK_NB); >+ if (rc) { >+ close(fd); >+ return; >+ } >+ >+ struct stat sb; >+ int res = fstat(fd, &sb); >+ size_t size = static_cast<size_t>(sb.st_size); >+ if (res || !size) { >+ close(fd); >+ return; >+ } >+ >+ void* buffer = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); >+ close(fd); >+ >+ m_cachedBytecode = JSC::CachedBytecode { buffer, size }; >+} >+ >+- (void)writeCache >+{ >+ if (m_cachedBytecode.size() || !m_cachePath) >+ return; >+ >+ JSC::ParserError error; >+ m_cachedBytecode = JSC::generateModuleBytecode(m_virtualMachine.vm, m_jsSourceCode->sourceCode(), error); >+ if (error.isValid()) >+ return; >+ int fd = open(m_cachePath.path.UTF8String, O_CREAT | O_WRONLY, 0666); >+ if (fd == -1) >+ return; >+ int rc = flock(fd, LOCK_EX | LOCK_NB); >+ if (!rc) >+ write(fd, m_cachedBytecode.data(), m_cachedBytecode.size()); >+ close(fd); >+} >+ >+@end >+ >+@implementation JSScript(Internal) >+ >+- (JSC::JSSourceCode*)jsSourceCode:(JSC::Identifier)moduleKey >+{ >+ if (m_jsSourceCode) { >+ ASSERT(moduleKey.impl() == m_moduleKey); >+ return m_jsSourceCode.get(); >+ } >+ >+ JSC::VM& vm = m_virtualMachine.vm; >+ TextPosition startPosition { }; >+ Ref<JSC::CachedBytecodeSourceProvider> sourceProvider = JSC::CachedBytecodeSourceProvider::create(m_source, &m_cachedBytecode, JSC::SourceOrigin(moduleKey.string()), URL({ }, moduleKey.string()), TextPosition(), JSC::SourceProviderSourceType::Module); >+ JSC::SourceCode sourceCode(WTFMove(sourceProvider), startPosition.m_line.oneBasedInt(), startPosition.m_column.oneBasedInt()); >+ JSC::JSSourceCode* jsSourceCode = JSC::JSSourceCode::create(vm, WTFMove(sourceCode)); >+ m_jsSourceCode.set(vm, jsSourceCode); >+ [self writeCache]; >+ return jsSourceCode; >+} > > @end > >diff --git a/Source/JavaScriptCore/API/JSScriptInternal.h b/Source/JavaScriptCore/API/JSScriptInternal.h >index a973ed1ad4a1fc5fdf35cf9482301795ae1f2eb3..f3303251b74d8297929207eb8f9f26befccefa97 100644 >--- a/Source/JavaScriptCore/API/JSScriptInternal.h >+++ b/Source/JavaScriptCore/API/JSScriptInternal.h >@@ -28,6 +28,13 @@ > #import "JSScript.h" > #import "SourceCode.h" > >-OBJC_CLASS JSScript; >+namespace JSC { >+class JSSourceCode; >+class Identifier; >+}; > >-const String& getJSScriptSourceCode(JSScript *); >+@interface JSScript(Internal) >+ >+- (JSC::JSSourceCode*)jsSourceCode:(JSC::Identifier)moduleKey; >+ >+@end >diff --git a/Source/JavaScriptCore/API/JSVirtualMachine.mm b/Source/JavaScriptCore/API/JSVirtualMachine.mm >index 5e795f016f05a381bdcf1040f335d0895395deaa..504d33cf5f7cb53f16dd37793fc61c915e23bfe6 100644 >--- a/Source/JavaScriptCore/API/JSVirtualMachine.mm >+++ b/Source/JavaScriptCore/API/JSVirtualMachine.mm >@@ -297,6 +297,11 @@ + (NSUInteger)setNumberOfFTLCompilerThreads:(NSUInteger)numberOfThreads > > #endif // ENABLE(DFG_JIT) > >+- (JSC::VM&)vm >+{ >+ return *toJS(m_group); >+} >+ > @end > > static void scanExternalObjectGraph(JSC::VM& vm, JSC::SlotVisitor& visitor, void* root, bool lockAcquired) >diff --git a/Source/JavaScriptCore/API/JSVirtualMachineInternal.h b/Source/JavaScriptCore/API/JSVirtualMachineInternal.h >index 9122ed49a4ea1aa49ec667fef6627fc43b77716d..9b0bf6beeb07add09c63e8b14034a88ce83d6ed7 100644 >--- a/Source/JavaScriptCore/API/JSVirtualMachineInternal.h >+++ b/Source/JavaScriptCore/API/JSVirtualMachineInternal.h >@@ -46,6 +46,8 @@ JSContextGroupRef getGroupFromVirtualMachine(JSVirtualMachine *); > > - (JSContext *)contextForGlobalContextRef:(JSGlobalContextRef)globalContext; > - (void)addContext:(JSContext *)wrapper forGlobalContextRef:(JSGlobalContextRef)globalContext; >+- (JSC::VM&)vm; >+ > @end > > #endif // defined(__OBJC__) >diff --git a/Source/JavaScriptCore/API/tests/testapi.mm b/Source/JavaScriptCore/API/tests/testapi.mm >index 13beb69f2bcd72194c70af568f9612d61ef6acc3..2289e6e3489ae2f6cdcebf56562a3113d696855b 100644 >--- a/Source/JavaScriptCore/API/tests/testapi.mm >+++ b/Source/JavaScriptCore/API/tests/testapi.mm >@@ -1980,6 +1980,68 @@ static void testImportModuleTwice() > } > } > >+static void testBytecodeCache() >+{ >+ @autoreleasepool { >+ NSURL* tempDirectory = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES]; >+ >+ NSString* fooSource = @"import { n } from \"../foo.js\"; export let foo = n;"; >+ NSString* barSource = @"import \"otherDirectory/baz.js\"; export let n = null;"; >+ NSString* bazSource = @"import { foo } from \"../directory/bar.js\"; globalThis.ran = null; export let exp = foo;"; >+ >+ NSURL* fooPath = [tempDirectory URLByAppendingPathComponent:@"foo.js"]; >+ NSURL* barPath = [tempDirectory URLByAppendingPathComponent:@"bar.js"]; >+ NSURL* bazPath = [tempDirectory URLByAppendingPathComponent:@"baz.js"]; >+ >+ NSURL* fooCachePath = [tempDirectory URLByAppendingPathComponent:@"foo.js.cache"]; >+ NSURL* barCachePath = [tempDirectory URLByAppendingPathComponent:@"bar.js.cache"]; >+ NSURL* bazCachePath = [tempDirectory URLByAppendingPathComponent:@"baz.js.cache"]; >+ >+ [fooSource writeToURL:fooPath atomically:NO encoding:NSASCIIStringEncoding error:nil]; >+ [barSource writeToURL:barPath atomically:NO encoding:NSASCIIStringEncoding error:nil]; >+ [bazSource writeToURL:bazPath atomically:NO encoding:NSASCIIStringEncoding error:nil]; >+ >+ __block bool forceDiskCache = false; >+ auto block = ^(JSContext *context, JSValue *identifier, JSValue *resolve, JSValue *reject) { >+ JSC::Options::forceDiskCache() = forceDiskCache; >+ if ([identifier isEqualToObject:@"file:///directory/bar.js"]) >+ [resolve callWithArguments:@[[JSScript scriptFromASCIIFile:fooPath inVirtualMachine:context.virtualMachine withCodeSigning:nil andBytecodeCache:fooCachePath]]]; >+ else if ([identifier isEqualToObject:@"file:///foo.js"]) >+ [resolve callWithArguments:@[[JSScript scriptFromASCIIFile:barPath inVirtualMachine:context.virtualMachine withCodeSigning:nil andBytecodeCache:barCachePath]]]; >+ else if ([identifier isEqualToObject:@"file:///otherDirectory/baz.js"]) >+ [resolve callWithArguments:@[[JSScript scriptFromASCIIFile:bazPath inVirtualMachine:context.virtualMachine withCodeSigning:nil andBytecodeCache:bazCachePath]]]; >+ else >+ [reject callWithArguments:@[[JSValue valueWithNewErrorFromMessage:@"Weird path" inContext:context]]]; >+ }; >+ >+ @autoreleasepool { >+ auto *context = [JSContextFetchDelegate contextWithBlockForFetch:block]; >+ context.moduleLoaderDelegate = context; >+ JSValue *promise = [context evaluateScript:@"import('../otherDirectory/baz.js');" withSourceURL:[NSURL fileURLWithPath:@"/directory" isDirectory:YES]]; >+ JSValue *null = [JSValue valueWithNullInContext:context]; >+ checkModuleCodeRan(context, promise, null); >+ } >+ >+ @autoreleasepool { >+ forceDiskCache = true; >+ auto *context = [JSContextFetchDelegate contextWithBlockForFetch:block]; >+ context.moduleLoaderDelegate = context; >+ JSValue *promise = [context evaluateScript:@"import('../otherDirectory/baz.js');" withSourceURL:[NSURL fileURLWithPath:@"/directory" isDirectory:YES]]; >+ JSValue *null = [JSValue valueWithNullInContext:context]; >+ checkModuleCodeRan(context, promise, null); >+ JSC::Options::forceDiskCache() = false; >+ } >+ >+ NSFileManager* fileManager = [NSFileManager defaultManager]; >+ [fileManager removeItemAtURL:fooPath error:nil]; >+ [fileManager removeItemAtURL:barPath error:nil]; >+ [fileManager removeItemAtURL:bazPath error:nil]; >+ [fileManager removeItemAtURL:fooCachePath error:nil]; >+ [fileManager removeItemAtURL:barCachePath error:nil]; >+ [fileManager removeItemAtURL:bazCachePath error:nil]; >+ } >+} >+ > @interface JSContextFileLoaderDelegate : JSContext <JSModuleLoaderDelegate> > > + (instancetype)newContext; >@@ -2017,7 +2079,7 @@ static NSURL *resolvePathToScripts() > - (void)context:(JSContext *)context fetchModuleForIdentifier:(JSValue *)identifier withResolveHandler:(JSValue *)resolve andRejectHandler:(JSValue *)reject > { > NSURL *filePath = [NSURL URLWithString:[identifier toString]]; >- auto *script = [JSScript scriptFromASCIIFile:filePath inVirtualMachine:[context virtualMachine] withCodeSigning:nil andBytecodeCache:nil]; >+ auto *script = [JSScript scriptFromASCIIFile:filePath inVirtualMachine:context.virtualMachine withCodeSigning:nil andBytecodeCache:nil]; > if (script) > [resolve callWithArguments:@[script]]; > else >@@ -2049,6 +2111,7 @@ void testObjectiveCAPI() > testFetchWithTwoCycle(); > testFetchWithThreeCycle(); > testImportModuleTwice(); >+ testBytecodeCache(); > > testLoaderRejectsNilScriptURL(); > testLoaderRejectsFailedFetch(); >diff --git a/Source/JavaScriptCore/bytecode/UnlinkedFunctionExecutable.cpp b/Source/JavaScriptCore/bytecode/UnlinkedFunctionExecutable.cpp >index b114d39e90226a510b2458521d95c63f75033eeb..e8dc1981f0e63c2601609d240b9a379719caa589 100644 >--- a/Source/JavaScriptCore/bytecode/UnlinkedFunctionExecutable.cpp >+++ b/Source/JavaScriptCore/bytecode/UnlinkedFunctionExecutable.cpp >@@ -194,6 +194,16 @@ UnlinkedFunctionExecutable* UnlinkedFunctionExecutable::fromGlobalCode( > return executable; > } > >+UnlinkedFunctionCodeBlock* UnlinkedFunctionExecutable::unlinkedCodeBlockFor(CodeSpecializationKind specializationKind) >+{ >+ switch (specializationKind) { >+ case CodeForCall: >+ return m_unlinkedCodeBlockForCall.get(); >+ case CodeForConstruct: >+ return m_unlinkedCodeBlockForConstruct.get(); >+ } >+} >+ > UnlinkedFunctionCodeBlock* UnlinkedFunctionExecutable::unlinkedCodeBlockFor( > VM& vm, const SourceCode& source, CodeSpecializationKind specializationKind, > DebuggerMode debuggerMode, ParserError& error, SourceParseMode parseMode) >diff --git a/Source/JavaScriptCore/bytecode/UnlinkedFunctionExecutable.h b/Source/JavaScriptCore/bytecode/UnlinkedFunctionExecutable.h >index 6526cd90cbbaa69e26cc0a1c0ac77b68c195fb56..d637d68af5ad631558ea93b3b86b9da04cb83175 100644 >--- a/Source/JavaScriptCore/bytecode/UnlinkedFunctionExecutable.h >+++ b/Source/JavaScriptCore/bytecode/UnlinkedFunctionExecutable.h >@@ -104,6 +104,8 @@ public: > unsigned typeProfilingEndOffset() const { return m_typeProfilingEndOffset; } > void setInvalidTypeProfilingOffsets(); > >+ UnlinkedFunctionCodeBlock* unlinkedCodeBlockFor(CodeSpecializationKind); >+ > UnlinkedFunctionCodeBlock* unlinkedCodeBlockFor( > VM&, const SourceCode&, CodeSpecializationKind, DebuggerMode, > ParserError&, SourceParseMode); >diff --git a/Source/JavaScriptCore/parser/SourceCodeKey.h b/Source/JavaScriptCore/parser/SourceCodeKey.h >index cc3abe21faf5714bf99801dfb9eed4283481f189..bc02f129abf185be7558fbd6354d593f86c8bb70 100644 >--- a/Source/JavaScriptCore/parser/SourceCodeKey.h >+++ b/Source/JavaScriptCore/parser/SourceCodeKey.h >@@ -100,6 +100,8 @@ public: > > unsigned hash() const { return m_hash; } > >+ const UnlinkedSourceCode& source() const { return m_sourceCode; } >+ > size_t length() const { return m_sourceCode.length(); } > > bool isNull() const { return m_sourceCode.isNull(); } >diff --git a/Source/JavaScriptCore/parser/SourceProvider.h b/Source/JavaScriptCore/parser/SourceProvider.h >index 09ad16834b0afb7be9f314afc2f250ad82edb5e5..7dbccee7616150b75052e92e179f815b750990ba 100644 >--- a/Source/JavaScriptCore/parser/SourceProvider.h >+++ b/Source/JavaScriptCore/parser/SourceProvider.h >@@ -1,5 +1,5 @@ > /* >- * Copyright (C) 2008, 2009, 2012, 2013 Apple Inc. All rights reserved. >+ * Copyright (C) 2008-2019 Apple Inc. All rights reserved. > * > * Redistribution and use in source and binary forms, with or without > * modification, are permitted provided that the following conditions >@@ -42,6 +42,43 @@ namespace JSC { > WebAssembly, > }; > >+ class CachedBytecode { >+ public: >+ CachedBytecode() >+ : CachedBytecode(nullptr, 0) >+ { >+ } >+ >+ CachedBytecode(const void* data, size_t size) >+ : m_owned(false) >+ , m_size(size) >+ , m_data(data) >+ { >+ } >+ >+ CachedBytecode(MallocPtr<uint8_t>&& data, size_t size) >+ : m_owned(true) >+ , m_size(size) >+ , m_data(data.leakPtr()) >+ { >+ } >+ >+ const void* data() const { return m_data; } >+ size_t size() const { return m_size; } >+ >+ ~CachedBytecode() >+ { >+ if (m_data && m_owned) >+ fastFree(const_cast<void*>(m_data)); >+ } >+ >+ private: >+ bool m_owned; >+ size_t m_size; >+ const void* m_data; >+ }; >+ >+ > class SourceProvider : public RefCounted<SourceProvider> { > public: > static const intptr_t nullID = 1; >@@ -52,6 +89,8 @@ namespace JSC { > > virtual unsigned hash() const = 0; > virtual StringView source() const = 0; >+ virtual const CachedBytecode* cachedBytecode() const { return nullptr; } >+ > StringView getRange(int start, int end) const > { > return source().substring(start, end - start); >@@ -104,15 +143,38 @@ namespace JSC { > return m_source.get(); > } > >- private: >+ protected: > StringSourceProvider(const String& source, const SourceOrigin& sourceOrigin, URL&& url, const TextPosition& startPosition, SourceProviderSourceType sourceType) > : SourceProvider(sourceOrigin, WTFMove(url), startPosition, sourceType) > , m_source(source.isNull() ? *StringImpl::empty() : *source.impl()) > { > } > >+ private: > Ref<StringImpl> m_source; > }; >+ >+ class CachedBytecodeSourceProvider : public StringSourceProvider { >+ public: >+ static Ref<CachedBytecodeSourceProvider> create(const String& source, const CachedBytecode* cachedBytecode, const SourceOrigin& sourceOrigin, URL&& url, const TextPosition& startPosition = TextPosition(), SourceProviderSourceType sourceType = SourceProviderSourceType::Program) >+ { >+ return adoptRef(*new CachedBytecodeSourceProvider(source, cachedBytecode, sourceOrigin, WTFMove(url), startPosition, sourceType)); >+ } >+ >+ const CachedBytecode* cachedBytecode() const override >+ { >+ return m_cachedBytecode; >+ } >+ >+ private: >+ CachedBytecodeSourceProvider(const String& source, const CachedBytecode* cachedBytecode, const SourceOrigin& sourceOrigin, URL&& url, const TextPosition& startPosition, SourceProviderSourceType sourceType) >+ : StringSourceProvider(source, sourceOrigin, WTFMove(url), startPosition, sourceType) >+ , m_cachedBytecode(cachedBytecode) >+ { >+ } >+ >+ const CachedBytecode* m_cachedBytecode; >+ }; > > #if ENABLE(WEBASSEMBLY) > class WebAssemblySourceProvider : public SourceProvider { >diff --git a/Source/JavaScriptCore/parser/UnlinkedSourceCode.h b/Source/JavaScriptCore/parser/UnlinkedSourceCode.h >index aaaad3f412f8f3fa5bc1f2febfb14cc74de22424..fae0e351eabe50fb8a4c539289f35b862d17db77 100644 >--- a/Source/JavaScriptCore/parser/UnlinkedSourceCode.h >+++ b/Source/JavaScriptCore/parser/UnlinkedSourceCode.h >@@ -81,6 +81,11 @@ namespace JSC { > > bool isHashTableDeletedValue() const { return m_provider.isHashTableDeletedValue(); } > >+ const SourceProvider& provider() const >+ { >+ return *m_provider; >+ } >+ > unsigned hash() const > { > ASSERT(m_provider); >diff --git a/Source/JavaScriptCore/runtime/CodeCache.cpp b/Source/JavaScriptCore/runtime/CodeCache.cpp >index c32d204b6cd8968181cb0d1df0eab83ec11819d4..895a840b9156ceb90397052ca0cd7116d3389156 100644 >--- a/Source/JavaScriptCore/runtime/CodeCache.cpp >+++ b/Source/JavaScriptCore/runtime/CodeCache.cpp >@@ -162,4 +162,72 @@ void CodeCache::write(VM& vm) > writeCodeBlock(vm, it.key, it.value); > } > >+void generateUnlinkedCodeBlockForFunctions(VM& vm, UnlinkedCodeBlock* unlinkedCodeBlock, const SourceCode& parentSource, DebuggerMode debuggerMode, ParserError& error) >+{ >+ auto generate = [&](UnlinkedFunctionExecutable* unlinkedExecutable, CodeSpecializationKind constructorKind) { >+ if (constructorKind == CodeForConstruct && SourceParseModeSet(SourceParseMode::AsyncArrowFunctionMode, SourceParseMode::AsyncMethodMode, SourceParseMode::AsyncFunctionMode).contains(unlinkedExecutable->parseMode())) >+ return; >+ >+ FunctionExecutable* executable = unlinkedExecutable->link(vm, parentSource); >+ const SourceCode& source = executable->source(); >+ UnlinkedFunctionCodeBlock* unlinkedFunctionCodeBlock = unlinkedExecutable->unlinkedCodeBlockFor(vm, source, constructorKind, debuggerMode, error, unlinkedExecutable->parseMode()); >+ if (unlinkedFunctionCodeBlock) >+ generateUnlinkedCodeBlockForFunctions(vm, unlinkedFunctionCodeBlock, source, debuggerMode, error); >+ }; >+ >+ for (unsigned i = 0; i < unlinkedCodeBlock->numberOfFunctionDecls(); i++) { >+ generate(unlinkedCodeBlock->functionDecl(i), CodeForCall); >+ generate(unlinkedCodeBlock->functionDecl(i), CodeForConstruct); >+ } >+ for (unsigned i = 0; i < unlinkedCodeBlock->numberOfFunctionExprs(); i++) { >+ generate(unlinkedCodeBlock->functionExpr(i), CodeForCall); >+ generate(unlinkedCodeBlock->functionExpr(i), CodeForConstruct); >+ } >+} >+ >+void writeCodeBlock(VM& vm, const SourceCodeKey& key, const SourceCodeValue& value) >+{ >+#if OS(DARWIN) >+ const char* cachePath = Options::diskCachePath(); >+ if (LIKELY(!cachePath)) >+ return; >+ >+ UnlinkedCodeBlock* codeBlock = jsDynamicCast<UnlinkedCodeBlock*>(vm, value.cell.get()); >+ if (!codeBlock) >+ return; >+ >+ unsigned hash = key.hash(); >+ char filename[512]; >+ int count = snprintf(filename, 512, "%s/%u.cache", cachePath, hash); >+ if (count < 0 || count > 512) >+ return; >+ >+ std::pair<MallocPtr<uint8_t>, size_t> result = encodeCodeBlock(vm, key, codeBlock); >+ >+ int fd = open(filename, O_CREAT | O_WRONLY, 0666); >+ if (fd == -1) >+ return; >+ int rc = flock(fd, LOCK_EX | LOCK_NB); >+ if (!rc) >+ ::write(fd, result.first.get(), result.second); >+ close(fd); >+#else >+ UNUSED_PARAM(vm); >+ UNUSED_PARAM(key); >+ UNUSED_PARAM(value); >+#endif >+} >+ >+CachedBytecode serializeBytecode(VM& vm, UnlinkedCodeBlock* codeBlock, const SourceCode& source, SourceCodeType codeType, JSParserStrictMode strictMode, JSParserScriptMode scriptMode, DebuggerMode debuggerMode) >+{ >+ SourceCodeKey key( >+ source, String(), codeType, strictMode, scriptMode, >+ DerivedContextType::None, EvalContextType::None, false, debuggerMode, >+ vm.typeProfiler() ? TypeProfilerEnabled::Yes : TypeProfilerEnabled::No, >+ vm.controlFlowProfiler() ? ControlFlowProfilerEnabled::Yes : ControlFlowProfilerEnabled::No, >+ WTF::nullopt); >+ std::pair<MallocPtr<uint8_t>, size_t> result = encodeCodeBlock(vm, key, codeBlock); >+ return CachedBytecode { WTFMove(result.first), result.second }; >+} >+ > } >diff --git a/Source/JavaScriptCore/runtime/CodeCache.h b/Source/JavaScriptCore/runtime/CodeCache.h >index 99395f2cd2841139a035c8b1589bb939c69c5702..986da053143997229b5522042a65260ed6614acd 100644 >--- a/Source/JavaScriptCore/runtime/CodeCache.h >+++ b/Source/JavaScriptCore/runtime/CodeCache.h >@@ -36,6 +36,7 @@ > #include "StrongInlines.h" > #include "UnlinkedCodeBlock.h" > #include "UnlinkedEvalCodeBlock.h" >+#include "UnlinkedFunctionCodeBlock.h" > #include "UnlinkedModuleProgramCodeBlock.h" > #include "UnlinkedProgramCodeBlock.h" > #include <sys/stat.h> >@@ -60,6 +61,15 @@ class UnlinkedProgramCodeBlock; > class VM; > class VariableEnvironment; > >+namespace CodeCacheInternal { >+static const bool verbose = false; >+} // namespace CodeCacheInternal >+ >+#define VERBOSE_LOG(...) do { \ >+ if (CodeCacheInternal::verbose) \ >+ dataLogLn("(JSC::CodeCache) ", __VA_ARGS__); \ >+} while (false) >+ > struct SourceCodeValue { > SourceCodeValue() > { >@@ -97,6 +107,16 @@ public: > template<typename UnlinkedCodeBlockType> > UnlinkedCodeBlockType* fetchFromDiskImpl(VM& vm, const SourceCodeKey& key) > { >+ { >+ const auto* cachedBytecode = key.source().provider().cachedBytecode(); >+ if (cachedBytecode && cachedBytecode->size()) { >+ VERBOSE_LOG("Found cached CodeBlock in the SourceProvider"); >+ UnlinkedCodeBlockType* unlinkedCodeBlock = decodeCodeBlock<UnlinkedCodeBlockType>(vm, key, cachedBytecode->data(), cachedBytecode->size()); >+ if (unlinkedCodeBlock) >+ return unlinkedCodeBlock; >+ } >+ } >+ > #if OS(DARWIN) > const char* cachePath = Options::diskCachePath(); > if (!cachePath) >@@ -121,15 +141,20 @@ public: > struct stat sb; > int res = fstat(fd, &sb); > size_t size = static_cast<size_t>(sb.st_size); >- if (res || !size) >+ if (res || !size) { >+ close(fd); > return nullptr; >+ } > >- const void* buffer = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); >+ void* buffer = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); > UnlinkedCodeBlockType* unlinkedCodeBlock = decodeCodeBlock<UnlinkedCodeBlockType>(vm, key, buffer, size); >+ munmap(buffer, size); > > if (!unlinkedCodeBlock) > return nullptr; > >+ >+ VERBOSE_LOG("Found cached CodeBlock on disk"); > addCache(key, SourceCodeValue(vm, unlinkedCodeBlock, m_age)); > return unlinkedCodeBlock; > #else >@@ -158,6 +183,7 @@ public: > { > prune(); > >+ VERBOSE_LOG("Trying to find cached CodeBlock for ", key.source().provider().url().string()); > iterator findResult = m_map.find(key); > if (findResult == m_map.end()) > return fetchFromDisk<UnlinkedCodeBlockType>(vm, key); >@@ -180,6 +206,7 @@ public: > findResult->value.age = m_age; > m_age += key.length(); > >+ VERBOSE_LOG("Found cached CodeBlock in memory"); > return jsCast<UnlinkedCodeBlockType*>(findResult->value.cell.get()); > } > >@@ -291,11 +318,10 @@ template <> struct CacheTypes<UnlinkedModuleProgramCodeBlock> { > static const SourceParseMode parseMode = SourceParseMode::ModuleEvaluateMode; > }; > >-template <class UnlinkedCodeBlockType, class ExecutableType> >-UnlinkedCodeBlockType* generateUnlinkedCodeBlock(VM& vm, ExecutableType* executable, const SourceCode& source, JSParserStrictMode strictMode, JSParserScriptMode scriptMode, DebuggerMode debuggerMode, ParserError& error, EvalContextType evalContextType, const VariableEnvironment* variablesUnderTDZ) >+template <class UnlinkedCodeBlockType, class ExecutableType = ScriptExecutable> >+UnlinkedCodeBlockType* generateUnlinkedCodeBlockImpl(VM& vm, const SourceCode& source, JSParserStrictMode strictMode, JSParserScriptMode scriptMode, DebuggerMode debuggerMode, ParserError& error, EvalContextType evalContextType, DerivedContextType derivedContextType, bool isArrowFunctionContext, const VariableEnvironment* variablesUnderTDZ, ExecutableType* executable = nullptr) > { > typedef typename CacheTypes<UnlinkedCodeBlockType>::RootNode RootNode; >- DerivedContextType derivedContextType = executable->derivedContextType(); > std::unique_ptr<RootNode> rootNode = parse<RootNode>( > &vm, source, Identifier(), JSParserBuiltinMode::NotBuiltin, strictMode, scriptMode, CacheTypes<UnlinkedCodeBlockType>::parseMode, SuperBinding::NotNeeded, error, nullptr, ConstructorKind::None, derivedContextType, evalContextType); > if (!rootNode) >@@ -306,10 +332,15 @@ UnlinkedCodeBlockType* generateUnlinkedCodeBlock(VM& vm, ExecutableType* executa > bool endColumnIsOnStartLine = !lineCount; > unsigned unlinkedEndColumn = rootNode->endColumn(); > unsigned endColumn = unlinkedEndColumn + (endColumnIsOnStartLine ? startColumn : 1); >- unsigned arrowContextFeature = executable->isArrowFunctionContext() ? ArrowFunctionContextFeature : 0; >- executable->recordParse(rootNode->features() | arrowContextFeature, rootNode->hasCapturedVariables(), rootNode->lastLine(), endColumn); >+ unsigned arrowContextFeature = isArrowFunctionContext ? ArrowFunctionContextFeature : 0; >+ if (executable) >+ executable->recordParse(rootNode->features() | arrowContextFeature, rootNode->hasCapturedVariables(), rootNode->lastLine(), endColumn); > >- UnlinkedCodeBlockType* unlinkedCodeBlock = UnlinkedCodeBlockType::create(&vm, executable->executableInfo(), debuggerMode); >+ bool usesEval = rootNode->features() & EvalFeature; >+ bool isStrictMode = rootNode->features() & StrictModeFeature; >+ ExecutableInfo executableInfo(usesEval, isStrictMode, false, false, ConstructorKind::None, scriptMode, SuperBinding::NotNeeded, CacheTypes<UnlinkedCodeBlockType>::parseMode, derivedContextType, isArrowFunctionContext, false, evalContextType); >+ >+ UnlinkedCodeBlockType* unlinkedCodeBlock = UnlinkedCodeBlockType::create(&vm, executableInfo, debuggerMode); > unlinkedCodeBlock->recordParse(rootNode->features(), rootNode->hasCapturedVariables(), lineCount, unlinkedEndColumn); > unlinkedCodeBlock->setSourceURLDirective(source.provider()->sourceURL()); > unlinkedCodeBlock->setSourceMappingURLDirective(source.provider()->sourceMappingURL()); >@@ -322,36 +353,28 @@ UnlinkedCodeBlockType* generateUnlinkedCodeBlock(VM& vm, ExecutableType* executa > return unlinkedCodeBlock; > } > >-ALWAYS_INLINE static void writeCodeBlock(VM& vm, const SourceCodeKey& key, const SourceCodeValue& value) >+template <class UnlinkedCodeBlockType, class ExecutableType> >+UnlinkedCodeBlockType* generateUnlinkedCodeBlock(VM& vm, ExecutableType* executable, const SourceCode& source, JSParserStrictMode strictMode, JSParserScriptMode scriptMode, DebuggerMode debuggerMode, ParserError& error, EvalContextType evalContextType, const VariableEnvironment* variablesUnderTDZ) >+{ >+ return generateUnlinkedCodeBlockImpl<UnlinkedCodeBlockType, ExecutableType>(vm, source, strictMode, scriptMode, debuggerMode, error, evalContextType, executable->derivedContextType(), executable->isArrowFunctionContext(), variablesUnderTDZ, executable); >+} >+ >+void generateUnlinkedCodeBlockForFunctions(VM&, UnlinkedCodeBlock*, const SourceCode&, DebuggerMode, ParserError&); >+ >+template <class UnlinkedCodeBlockType> >+std::enable_if_t<!std::is_same<UnlinkedCodeBlockType, UnlinkedEvalCodeBlock>::value, UnlinkedCodeBlockType*> >+recursivelyGenerateUnlinkedCodeBlock(VM& vm, const SourceCode& source, JSParserStrictMode strictMode, JSParserScriptMode scriptMode, DebuggerMode debuggerMode, ParserError& error, EvalContextType evalContextType, const VariableEnvironment* variablesUnderTDZ) > { >-#if OS(DARWIN) >- const char* cachePath = Options::diskCachePath(); >- if (LIKELY(!cachePath)) >- return; >- >- UnlinkedCodeBlock* codeBlock = jsDynamicCast<UnlinkedCodeBlock*>(vm, value.cell.get()); >- if (!codeBlock) >- return; >- >- unsigned hash = key.hash(); >- char filename[512]; >- int count = snprintf(filename, 512, "%s/%u.cache", cachePath, hash); >- if (count < 0 || count > 512) >- return; >- >- std::pair<MallocPtr<uint8_t>, size_t> result = encodeCodeBlock(vm, key, codeBlock); >- >- int fd = open(filename, O_CREAT | O_WRONLY, 0666); >- int rc = flock(fd, LOCK_EX | LOCK_NB); >- if (!rc) >- ::write(fd, result.first.get(), result.second); >- close(fd); >-#else >- UNUSED_PARAM(vm); >- UNUSED_PARAM(key); >- UNUSED_PARAM(value); >-#endif >+ bool isArrowFunctionContext = false; >+ UnlinkedCodeBlockType* unlinkedCodeBlock = generateUnlinkedCodeBlockImpl<UnlinkedCodeBlockType>(vm, source, strictMode, scriptMode, debuggerMode, error, evalContextType, DerivedContextType::None, isArrowFunctionContext, variablesUnderTDZ); >+ if (!unlinkedCodeBlock) >+ return nullptr; >+ >+ generateUnlinkedCodeBlockForFunctions(vm, unlinkedCodeBlock, source, debuggerMode, error); >+ return unlinkedCodeBlock; > } > >+void writeCodeBlock(VM&, const SourceCodeKey&, const SourceCodeValue&); >+CachedBytecode serializeBytecode(VM&, UnlinkedCodeBlock*, const SourceCode&, SourceCodeType, JSParserStrictMode, JSParserScriptMode, DebuggerMode); > > } // namespace JSC >diff --git a/Source/JavaScriptCore/runtime/Completion.cpp b/Source/JavaScriptCore/runtime/Completion.cpp >index 3fb4ab9d3aa2e34bbcd9adffa7307e7076a461af..8a875cd7e3b05dc43659f01b70052bc13cc1ae19 100644 >--- a/Source/JavaScriptCore/runtime/Completion.cpp >+++ b/Source/JavaScriptCore/runtime/Completion.cpp >@@ -25,6 +25,7 @@ > > #include "CallFrame.h" > #include "CatchScope.h" >+#include "CodeCache.h" > #include "CodeProfiling.h" > #include "Exception.h" > #include "IdentifierInlines.h" >@@ -90,6 +91,36 @@ bool checkModuleSyntax(ExecState* exec, const SourceCode& source, ParserError& e > return true; > } > >+CachedBytecode generateBytecode(VM& vm, const SourceCode& source, ParserError& error) >+{ >+ JSLockHolder lock(vm); >+ RELEASE_ASSERT(vm.atomicStringTable() == Thread::current().atomicStringTable()); >+ >+ VariableEnvironment variablesUnderTDZ; >+ JSParserStrictMode strictMode = JSParserStrictMode::NotStrict; >+ JSParserScriptMode scriptMode = JSParserScriptMode::Classic; >+ DebuggerMode debuggerMode = DebuggerOff; >+ EvalContextType evalContextType = EvalContextType::None; >+ >+ UnlinkedCodeBlock* unlinkedCodeBlock = recursivelyGenerateUnlinkedCodeBlock<UnlinkedProgramCodeBlock>(vm, source, strictMode, scriptMode, debuggerMode, error, evalContextType, &variablesUnderTDZ); >+ return serializeBytecode(vm, unlinkedCodeBlock, source, SourceCodeType::ProgramType, strictMode, scriptMode, debuggerMode); >+} >+ >+CachedBytecode generateModuleBytecode(VM& vm, const SourceCode& source, ParserError& error) >+{ >+ JSLockHolder lock(vm); >+ RELEASE_ASSERT(vm.atomicStringTable() == Thread::current().atomicStringTable()); >+ >+ VariableEnvironment variablesUnderTDZ; >+ JSParserStrictMode strictMode = JSParserStrictMode::Strict; >+ JSParserScriptMode scriptMode = JSParserScriptMode::Module; >+ DebuggerMode debuggerMode = DebuggerOff; >+ EvalContextType evalContextType = EvalContextType::None; >+ >+ UnlinkedCodeBlock* unlinkedCodeBlock = recursivelyGenerateUnlinkedCodeBlock<UnlinkedModuleProgramCodeBlock>(vm, source, strictMode, scriptMode, debuggerMode, error, evalContextType, &variablesUnderTDZ); >+ return serializeBytecode(vm, unlinkedCodeBlock, source, SourceCodeType::ModuleType, strictMode, scriptMode, debuggerMode); >+} >+ > JSValue evaluate(ExecState* exec, const SourceCode& source, JSValue thisValue, NakedPtr<Exception>& returnedException) > { > VM& vm = exec->vm(); >diff --git a/Source/JavaScriptCore/runtime/Completion.h b/Source/JavaScriptCore/runtime/Completion.h >index 827e61f3c61d0cb7bbe75a61277124fb0da72aae..3072889bb4caa36b55d43f1c66a632d64138196c 100644 >--- a/Source/JavaScriptCore/runtime/Completion.h >+++ b/Source/JavaScriptCore/runtime/Completion.h >@@ -28,6 +28,7 @@ > > namespace JSC { > >+class CachedBytecode; > class Exception; > class ExecState; > class JSObject; >@@ -41,6 +42,9 @@ JS_EXPORT_PRIVATE bool checkSyntax(VM&, const SourceCode&, ParserError&); > JS_EXPORT_PRIVATE bool checkSyntax(ExecState*, const SourceCode&, JSValue* exception = 0); > JS_EXPORT_PRIVATE bool checkModuleSyntax(ExecState*, const SourceCode&, ParserError&); > >+JS_EXPORT_PRIVATE CachedBytecode generateBytecode(VM&, const SourceCode&, ParserError&); >+JS_EXPORT_PRIVATE CachedBytecode generateModuleBytecode(VM&, const SourceCode&, ParserError&); >+ > JS_EXPORT_PRIVATE JSValue evaluate(ExecState*, const SourceCode&, JSValue thisValue, NakedPtr<Exception>& returnedException); > inline JSValue evaluate(ExecState* exec, const SourceCode& sourceCode, JSValue thisValue = JSValue()) > { >diff --git a/Source/JavaScriptCore/runtime/Options.cpp b/Source/JavaScriptCore/runtime/Options.cpp >index 8b05ca82300bc79a229ac60f86bb45667eefda5e..84d7326a24fbe5a5de03c5c38dbf2bdedf2360cb 100644 >--- a/Source/JavaScriptCore/runtime/Options.cpp >+++ b/Source/JavaScriptCore/runtime/Options.cpp >@@ -527,9 +527,6 @@ static void recomputeDependentOptions() > > if (!Options::useCodeCache()) > Options::diskCachePath() = nullptr; >- >- if (!Options::diskCachePath()) >- Options::forceDiskCache() = false; > } > > void Options::initialize()
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Formatted Diff
|
Diff
Attachments on
bug 193401
:
359045
|
359899
|
359954
|
360008
|
360040
|
360114
|
360158