Skip to content

Commit

Permalink
feat: optimize bytecode cache load speed and fix http cache (#552)
Browse files Browse the repository at this point in the history
1. The HTTP cache relies on the contextId to function. However, this
[PR](#500) introduced a bug that
make the entire HTTP cache mode ineffective.
2. According to profile data, we found that the
`getCrc32(bytes.toList())` takes lots of time with unexpected and can be
optimized with `getCrc32(bytes)` instead.


<img width="761" alt="image"
src="https://github.com/openwebf/webf/assets/4409743/7f934923-894d-431d-b43d-8e4c9e3d4baa">

3. The`CookieJar.setCookieString` also take lots of times unexpected and
can be optimized by making the file write async.
<img width="650" alt="image"
src="https://github.com/openwebf/webf/assets/4409743/a103bb0c-a95b-4387-be08-bdcd694cf73e">
  • Loading branch information
andycall authored Mar 20, 2024
1 parent 39c476d commit 6a0bef8
Show file tree
Hide file tree
Showing 16 changed files with 53 additions and 382 deletions.
4 changes: 2 additions & 2 deletions integration_tests/specs/css/css-transitions/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ describe('Transition all', () => {
});
});

it('toggle the transition class should the previous animation and run new animations', async (done) => {
fit('toggle the transition class should the previous animation and run new animations', async (done) => {
const container1 = document.createElement('div');
document.body.appendChild(container1);
setElementStyle(container1, {
Expand Down Expand Up @@ -354,7 +354,7 @@ describe('Transition all', () => {

await snapshot();
}, 500);
}, 500);
}, 200);
});
});
});
1 change: 0 additions & 1 deletion webf/lib/painting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@
* Copyright (C) 2022-present The WebF authors. All rights reserved.
*/

export 'src/painting/cached_network_image.dart';
export 'src/painting/image_provider_factory.dart';
export 'src/painting/box_fit_image.dart';
11 changes: 6 additions & 5 deletions webf/lib/src/bridge/to_native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -259,20 +259,21 @@ class ScriptByteCode {

class _EvaluateScriptsContext {
Completer completer;
String? cacheKey;
Pointer<Uint8> codePtr;
Pointer<Utf8> url;
Pointer<Pointer<Uint8>>? bytecodes;
Pointer<Uint64>? bytecodeLen;
Uint8List originalCodeBytes;

_EvaluateScriptsContext(this.completer, this.originalCodeBytes, this.codePtr, this.url);
_EvaluateScriptsContext(this.completer, this.originalCodeBytes, this.codePtr, this.url, this.cacheKey);
}

void handleEvaluateScriptsResult(_EvaluateScriptsContext context, int result) {
if (context.bytecodes != null) {
Uint8List bytes = context.bytecodes!.value.asTypedList(context.bytecodeLen!.value);
// Save to disk cache
QuickJSByteCodeCache.putObject(context.originalCodeBytes, bytes).then((_) {
QuickJSByteCodeCache.putObject(context.originalCodeBytes, bytes, cacheKey: context.cacheKey).then((_) {
malloc.free(context.codePtr);
malloc.free(context.url);
context.completer.complete(result == 1);
Expand All @@ -284,7 +285,7 @@ void handleEvaluateScriptsResult(_EvaluateScriptsContext context, int result) {
}
}

Future<bool> evaluateScripts(double contextId, Uint8List codeBytes, {String? url, int line = 0}) async {
Future<bool> evaluateScripts(double contextId, Uint8List codeBytes, {String? url, String? cacheKey, int line = 0}) async {
if (WebFController.getControllerOfJSContextId(contextId) == null) {
return false;
}
Expand All @@ -294,7 +295,7 @@ Future<bool> evaluateScripts(double contextId, Uint8List codeBytes, {String? url
_anonymousScriptEvaluationId++;
}

QuickJSByteCodeCacheObject cacheObject = await QuickJSByteCodeCache.getCacheObject(codeBytes);
QuickJSByteCodeCacheObject cacheObject = await QuickJSByteCodeCache.getCacheObject(codeBytes, cacheKey: cacheKey);
if (QuickJSByteCodeCacheObject.cacheMode == ByteCodeCacheMode.DEFAULT &&
cacheObject.valid &&
cacheObject.bytes != null) {
Expand All @@ -310,7 +311,7 @@ Future<bool> evaluateScripts(double contextId, Uint8List codeBytes, {String? url
Pointer<Uint8> codePtr = uint8ListToPointer(codeBytes);
Completer<bool> completer = Completer();

_EvaluateScriptsContext context = _EvaluateScriptsContext(completer, codeBytes, codePtr, _url);
_EvaluateScriptsContext context = _EvaluateScriptsContext(completer, codeBytes, codePtr, _url, cacheKey);
Pointer<NativeFunction<NativeEvaluateJavaScriptCallback>> resultCallback =
Pointer.fromFunction(handleEvaluateScriptsResult);

Expand Down
2 changes: 0 additions & 2 deletions webf/lib/src/css/background.dart
Original file line number Diff line number Diff line change
Expand Up @@ -478,8 +478,6 @@ class CSSBackgroundImage {
switch (image.runtimeType) {
case NetworkImage:
return (image as NetworkImage).url;
case CachedNetworkImage:
return (image as CachedNetworkImage).url;
case FileImage:
return (image as FileImage).file.uri.path;
case MemoryImage:
Expand Down
2 changes: 1 addition & 1 deletion webf/lib/src/css/font_face.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class CSSFontFace {
final WebFController controller = WebFController.getControllerOfJSContextId(contextId)!;
WebFBundle bundle = controller.getPreloadBundleFromUrl(uri.toString()) ?? WebFBundle.fromUrl(uri.toString());
await bundle.resolve(baseUrl: controller.url, uriParser: controller.uriParser);
await bundle.obtainData();
await bundle.obtainData(controller.view.contextId);
assert(bundle.isResolved, 'Failed to obtain $url');
FontLoader loader = FontLoader(removeQuotationMark(fontFamily));
Future<ByteData> bytes = Future.value(bundle.data?.buffer.asByteData());
Expand Down
21 changes: 16 additions & 5 deletions webf/lib/src/foundation/bundle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ abstract class WebFBundle {
bool get isResolved => _uri != null;
bool get isDataObtained => data != null;

bool _hitCache = false;
set hitCache(bool value) => _hitCache = value;

String? get cacheKey {
if (!_hitCache) return null;
return HttpCacheController.getCacheKey(resolvedUri!).hashCode.toString();
}

// Content type for data.
// The default value is plain text.
ContentType? _contentType;
Expand Down Expand Up @@ -127,7 +135,7 @@ abstract class WebFBundle {
}
}

Future<void> obtainData();
Future<void> obtainData(double contextId);

// Dispose the memory obtained by bundle.
@mustCallSuper
Expand Down Expand Up @@ -217,7 +225,7 @@ class DataBundle extends WebFBundle {
}

@override
Future<void> obtainData() async {}
Future<void> obtainData(double contextId) async {}
}

// The bundle that source from http or https.
Expand All @@ -231,21 +239,24 @@ class NetworkBundle extends WebFBundle {
Map<String, String>? additionalHttpHeaders = {};

@override
Future<void> obtainData() async {
Future<void> obtainData(double contextId) async {
if (data != null) return;

final HttpClientRequest request = await _sharedHttpClient.getUrl(_uri!);

// Prepare request headers.
request.headers.set('Accept', _acceptHeader());
additionalHttpHeaders?.forEach(request.headers.set);
WebFHttpOverrides.setContextHeader(request.headers, contextId);

final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok)
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Unable to load asset: $url'),
IntProperty('HTTP status code', response.statusCode),
]);

hitCache = response is HttpClientStreamResponse || response is HttpClientCachedResponse;
Uint8List bytes = await consolidateHttpClientResponseBytes(response);

// To maintain compatibility with older versions of WebF, which save Gzip content in caches, we should check the bytes
Expand All @@ -268,7 +279,7 @@ class AssetsBundle extends WebFBundle {
AssetsBundle(String url, { ContentType? contentType }) : super(url, contentType: contentType);

@override
Future<void> obtainData() async {
Future<void> obtainData(double contextId) async {
if (data != null) return;

final Uri? _resolvedUri = resolvedUri;
Expand Down Expand Up @@ -300,7 +311,7 @@ class FileBundle extends WebFBundle {
FileBundle(String url, { ContentType? contentType }) : super(url, contentType: contentType);

@override
Future<void> obtainData() async {
Future<void> obtainData(double contextId) async {
if (data != null) return;

Uri uri = _uri!;
Expand Down
26 changes: 12 additions & 14 deletions webf/lib/src/foundation/bytecode_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class QuickJSByteCodeCacheObject {

try {
bytes = await cacheFile.readAsBytes();
int fileCheckSum = getCrc32(bytes!.toList());
int fileCheckSum = getCrc32(bytes as List<int>);

bool isCheckSumExist = await _checksum.exists();

Expand Down Expand Up @@ -87,7 +87,7 @@ class QuickJSByteCodeCacheObject {

Future<void> write() async {
if (bytes != null) {
int fileSum = getCrc32(bytes!.toList());
int fileSum = getCrc32(bytes!);
File tmp = File(path.join(cacheDirectory, '$hash.tmp'));

await Future.wait([
Expand Down Expand Up @@ -133,19 +133,22 @@ class QuickJSByteCodeCache {
return _cacheDirectory = cacheDirectory;
}

static String _getCacheHash(Uint8List code) {
static String _getCacheHashSlow(Uint8List code) {
WebFInfo webFInfo = getWebFInfo();
// Uri uriWithoutFragment = uri;
// return uriWithoutFragment.toString();
return '%${hashObjects(code)}_${webFInfo.appRevision}%';
}

static String _getCacheHashFast(String originalCacheKey) {
WebFInfo webFInfo = getWebFInfo();
return '%${originalCacheKey}_${webFInfo.appRevision}%';
}

// Get the CacheObject by uri, no validation needed here.
static Future<QuickJSByteCodeCacheObject> getCacheObject(Uint8List codeBytes) async {
static Future<QuickJSByteCodeCacheObject> getCacheObject(Uint8List codeBytes, { String? cacheKey }) async {
QuickJSByteCodeCacheObject cacheObject;

// L2 cache in memory.
final String hash = _getCacheHash(codeBytes);
final String hash = cacheKey != null ? _getCacheHashFast(cacheKey) : _getCacheHashSlow(codeBytes);
if (_caches.containsKey(hash)) {
cacheObject = _caches[hash]!;
} else {
Expand All @@ -160,8 +163,8 @@ class QuickJSByteCodeCache {
}

// Add or update the httpCacheObject to memory cache.
static Future<void> putObject(Uint8List codeBytes, Uint8List bytes) async {
final String key = _getCacheHash(codeBytes);
static Future<void> putObject(Uint8List codeBytes, Uint8List bytes, { String? cacheKey }) async {
final String key = cacheKey != null ? _getCacheHashFast(cacheKey) : _getCacheHashSlow(codeBytes);

final Directory cacheDirectory = await getCacheDirectory();
QuickJSByteCodeCacheObject cacheObject =
Expand All @@ -171,11 +174,6 @@ class QuickJSByteCodeCache {
await cacheObject.write();
}

static void removeObject(Uint8List code) {
final String key = _getCacheHash(code);
_caches.remove(key);
}

static bool isCodeNeedCache(Uint8List codeBytes) {
return QuickJSByteCodeCacheObject.cacheMode == ByteCodeCacheMode.DEFAULT &&
codeBytes.length > 1024 * 10; // >= 50 KB
Expand Down
6 changes: 3 additions & 3 deletions webf/lib/src/foundation/cookie_jar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class CookieJar {

static Future<PersistCookieJar> afterCookieJarLoaded(PersistCookieJar cookieJar, { Uri? uri, List<Cookie>? initialCookies }) async {
if (initialCookies != null && uri != null) {
cookieJar.saveFromAPISync(uri, initialCookies);
cookieJar.saveFromAPI(uri, initialCookies);
}
_cookieJar = cookieJar;
return cookieJar;
Expand All @@ -53,14 +53,14 @@ class CookieJar {
cookie.domain ??= uri.host;

if (uri.host.isNotEmpty && _cookieJar != null) {
_cookieJar!.saveFromAPISync(uri, [cookie]);
_cookieJar!.saveFromAPI(uri, [cookie]);
}
}

void setCookie(List<Cookie> cookies, [Uri? uri]) {
if (_cookieJar != null) {
uri = uri ?? Uri.parse(url);
_cookieJar!.saveFromAPISync(uri, cookies);
_cookieJar!.saveFromAPI(uri, cookies);
}
}

Expand Down
26 changes: 3 additions & 23 deletions webf/lib/src/foundation/cookie_jar/persist_cookie_jar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -169,14 +169,14 @@ class PersistCookieJar extends DefaultCookieJar {
}
}

void saveFromAPISync(Uri uri, List<Cookie> cookies) {
void saveFromAPI(Uri uri, List<Cookie> cookies) {
_checkInitializedSync();
if (cookies.isNotEmpty) {
saveCookiesToMemory(uri, cookies);
if (cookies.every((Cookie e) => e.domain == null)) {
_saveSync(uri);
_save(uri);
} else {
_saveSync(uri, true);
_save(uri, true);
}
}
}
Expand Down Expand Up @@ -265,26 +265,6 @@ class PersistCookieJar extends DefaultCookieJar {
}
}

void _saveSync(Uri uri, [bool withDomainSharedCookie = false]) {
final host = uri.host;

if (!_hostSet.contains(host)) {
_hostSet.add(host);
storage.writeSync(IndexKey, json.encode(_hostSet.toList()));
}
final cookies = hostCookies[host];

if (cookies != null) {
storage.writeSync(host, json.encode(_filter(cookies)));
}

if (withDomainSharedCookie) {
var filterDomainCookies =
domainCookies.map((key, value) => MapEntry(key, _filter(value)));
storage.writeSync(DomainsKey, json.encode(filterDomainCookies));
}
}

Future<void> _load(Uri uri) async {
final host = uri.host;
if (_hostSet.contains(host) && hostCookies[host] == null) {
Expand Down
10 changes: 5 additions & 5 deletions webf/lib/src/foundation/http_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class HttpCacheController {
return _cacheDirectory = cacheDirectory;
}

static String _getCacheKey(Uri uri) {
static String getCacheKey(Uri uri) {
// Fragment not included in cache.
Uri uriWithoutFragment = uri;
if (uriWithoutFragment.hasFragment) {
Expand Down Expand Up @@ -81,7 +81,7 @@ class HttpCacheController {
HttpCacheObject cacheObject;

// L2 cache in memory.
final String key = _getCacheKey(uri);
final String key = getCacheKey(uri);
if (_caches.containsKey(key)) {
cacheObject = _caches[key]!;
} else {
Expand All @@ -101,12 +101,12 @@ class HttpCacheController {
if (_caches.length == _maxCachedObjects) {
_caches.remove(_caches.lastKey());
}
final String key = _getCacheKey(uri);
final String key = getCacheKey(uri);
_caches.update(key, (value) => cacheObject, ifAbsent: () => cacheObject);
}

void removeObject(Uri uri) {
final String key = _getCacheKey(uri);
final String key = getCacheKey(uri);
_caches.remove(key);
}

Expand All @@ -125,7 +125,7 @@ class HttpCacheController {
if (response.statusCode == HttpStatus.ok) {
// Create cache object.
HttpCacheObject cacheObject =
HttpCacheObject.fromResponse(_getCacheKey(request.uri), response, (await getCacheDirectory()).path);
HttpCacheObject.fromResponse(getCacheKey(request.uri), response, (await getCacheDirectory()).path);

// Cache the object.
if (cacheObject.valid) {
Expand Down
2 changes: 1 addition & 1 deletion webf/lib/src/html/head.dart
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class LinkElement extends Element {
ownerDocument.incrementRequestCount();

await bundle.resolve(baseUrl: ownerDocument.controller.url, uriParser: ownerDocument.controller.uriParser);
await bundle.obtainData();
await bundle.obtainData(ownerView.contextId);
assert(bundle.isResolved, 'Failed to obtain $url');
_loading = false;
// Decrement count when response.
Expand Down
2 changes: 1 addition & 1 deletion webf/lib/src/html/img.dart
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@ class ImageRequest {
final WebFBundle bundle =
controller.getPreloadBundleFromUrl(currentUri.toString()) ?? WebFBundle.fromUrl(currentUri.toString());
await bundle.resolve(baseUrl: controller.url, uriParser: controller.uriParser);
await bundle.obtainData();
await bundle.obtainData(controller.view.contextId);

if (!bundle.isResolved) {
throw FlutterError('Failed to load $currentUri');
Expand Down
4 changes: 2 additions & 2 deletions webf/lib/src/html/script.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class ScriptRunner {
// Evaluate bundle.
if (bundle.isJavascript) {
assert(isValidUTF8String(bundle.data!), 'The JavaScript codes should be in UTF-8 encoding format');
bool result = await evaluateScripts(contextId, bundle.data!, url: bundle.url);
bool result = await evaluateScripts(contextId, bundle.data!, url: bundle.url, cacheKey: bundle.cacheKey);
if (!result) {
throw FlutterError('Script code are not valid to evaluate.');
}
Expand Down Expand Up @@ -147,7 +147,7 @@ class ScriptRunner {
_document.incrementDOMContentLoadedEventDelayCount();
try {
await bundle.resolve(baseUrl: _document.controller.url, uriParser: _document.controller.uriParser);
await bundle.obtainData();
await bundle.obtainData(_contextId);

if (!bundle.isResolved) {
throw FlutterError('Network error.');
Expand Down
2 changes: 1 addition & 1 deletion webf/lib/src/launcher/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1448,7 +1448,7 @@ class WebFController {
// Resolve the bundle, including network download or other fetching ways.
try {
await bundleToLoad.resolve(baseUrl: url, uriParser: uriParser);
await bundleToLoad.obtainData();
await bundleToLoad.obtainData(view.contextId);
} catch (e, stack) {
if (onLoadError != null) {
onLoadError!(FlutterError(e.toString()), stack);
Expand Down
Loading

0 comments on commit 6a0bef8

Please sign in to comment.