Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance and correctness of chrome debugging #2373

Merged
merged 18 commits into from
May 16, 2019
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
x.x.x Release notes (yyyy-MM-dd)
=============================================================
### Enhancements
* Improve performance when using Chrome Debugging with React Native by adding caching and reducing the number of RPC calls required. Read-heavy workflows are as much as 10x faster. Write-heavy workflows will see a much smaller improvement, but also had a smaller performance hit to begin with. (PR: ([#2373](https://github.com/realm/realm-js/pull/2373))).
tgoyne marked this conversation as resolved.
Show resolved Hide resolved

### Fixed
* <How to hit and notice issue? what was the impact?> ([#????](https://github.com/realm/realm-js/issues/????), since v?.?.?)
* Opening a query-based Realm using `new Realm` did not automatically add the required types to the schema when running in Chrome, resulting in errors when trying to manage subscriptions. (PR: ([#2373](https://github.com/realm/realm-js/pull/2373))).
tgoyne marked this conversation as resolved.
Show resolved Hide resolved
* The Chrome debugger did not properly enforce read isolation, meaning that reading a property twice in a row could produce different values if another thread performed a write in between the reads. This was typically only relevant to synchronized Realms due to the lack of multithreading support in the supported Javascript environments. (PR: ([#2373](https://github.com/realm/realm-js/pull/2373))).
* The RPC server for Chrome debugging would sometimes deadlock if a notification fired at the same time as a Realm function which takes a callback was called. (PR: ([#2373](https://github.com/realm/realm-js/pull/2373))).
tgoyne marked this conversation as resolved.
Show resolved Hide resolved

### Compatibility
* Realm Object Server: 3.11.0 or later.
tgoyne marked this conversation as resolved.
Show resolved Hide resolved
* APIs are backwards compatible with all previous release of realm in the 2.x.y series.
* File format: Generates Realms with format v9 (Reads and upgrades all previous formats)

### Internal
* None.

2.27.0 Release notes (2019-5-15)
=============================================================
NOTE: The minimum version of Realm Object Server has been increased to 3.21.0 and attempting to connect to older versions will produce protocol mismatch errors. Realm Cloud has already been upgraded to this version, and users using that do not need to worry about this.
Expand Down
46 changes: 8 additions & 38 deletions lib/browser/collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,40 +19,15 @@
'use strict';

import { keys } from './constants';
import { getterForProperty } from './util';
import { getProperty, setProperty } from './rpc';

let mutationListeners = {};
import * as util from './util';
import * as rpc from './rpc';

export default class Collection {
constructor() {
throw new TypeError('Illegal constructor');
}
}

export function addMutationListener(realmId, callback) {
let listeners = mutationListeners[realmId] || (mutationListeners[realmId] = new Set());
listeners.add(callback);
}

export function removeMutationListener(realmId, callback) {
let listeners = mutationListeners[realmId];
if (listeners) {
listeners.delete(callback);
}
}

export function clearMutationListeners() {
mutationListeners = {};
}

export function fireMutationListeners(realmId) {
let listeners = mutationListeners[realmId];
if (listeners) {
listeners.forEach((cb) => cb());
}
}

function isIndex(propertyName) {
return typeof propertyName === 'number' || (typeof propertyName === 'string' && /^-?\d+$/.test(propertyName));
}
Expand All @@ -62,7 +37,7 @@ const mutable = Symbol('mutable');
const traps = {
get(collection, property, receiver) {
if (isIndex(property)) {
return getProperty(collection[keys.realm], collection[keys.id], property);
return util.getProperty(collection, property);
}

return Reflect.get(collection, property, collection);
Expand All @@ -73,13 +48,8 @@ const traps = {
return false;
}

setProperty(collection[keys.realm], collection[keys.id], property, value);

// If this isn't a primitive value, then it might create a new object in the Realm.
if (value && typeof value == 'object') {
fireMutationListeners(collection[keys.realm]);
}

util.invalidateCache(collection[keys.realm]);
rpc.setProperty(collection[keys.realm], collection[keys.id], property, value);
return true;
}

Expand Down Expand Up @@ -118,13 +88,13 @@ export function createCollection(prototype, realmId, info, _mutable) {

Object.defineProperties(collection, {
'length': {
get: getterForProperty('length'),
get: util.getterForProperty('length'),
},
'type': {
get: getterForProperty('type'),
value: info.dataType,
},
'optional': {
get: getterForProperty('optional'),
value: info.optional,
},
});

Expand Down
51 changes: 31 additions & 20 deletions lib/browser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

import { NativeModules } from 'react-native';
import { keys, objectTypes } from './constants';
import Collection, * as collections from './collections';
import Collection from './collections';
import List, { createList } from './lists';
import Results, { createResults } from './results';
import RealmObject, * as objects from './objects';
Expand All @@ -43,30 +43,27 @@ rpc.registerTypeConverter(objectTypes.SUBSCRIPTION, createSubscription);

function createRealm(_, info) {
let realm = Object.create(Realm.prototype);

setupRealm(realm, info.id);
setupRealm(realm, info);
return realm;
}

function setupRealm(realm, realmId) {
realm[keys.id] = realmId;
realm[keys.realm] = realmId;
function setupRealm(realm, info) {
realm[keys.id] = info.id;
realm[keys.realm] = info.realmId;
realm[keys.type] = objectTypes.REALM;

[
'empty',
'path',
'readOnly',
'inMemory',
'schema',
'schemaVersion',
'syncSession',
'isInTransaction',
'isClosed',
'_isPartialRealm',
].forEach((name) => {
Object.defineProperty(realm, name, {get: util.getterForProperty(name)});
});
for (let key in info.data) {
realm[key] = rpc.deserialize(info.id, info.data[key]);
}
}

function getObjectType(realm, type) {
Expand All @@ -78,9 +75,23 @@ function getObjectType(realm, type) {

export default class Realm {
constructor(config) {
let schemas = typeof config == 'object' && config.schema;
let schemas = typeof config === 'object' && config.schema;
let constructors = schemas ? {} : null;

let isPartial = false;
if (config && typeof config.sync === 'object') {
if (typeof config.sync.fullSynchronization !== 'undefined') {
isPartial = !config.sync.fullSynchronization;
}
else if (typeof config.sync.partial !== 'undefined') {
isPartial = config.sync.partial;
}
}

if (schemas && isPartial) {
Realm._extendQueryBasedSchema(schemas);
}

for (let i = 0, len = schemas ? schemas.length : 0; i < len; i++) {
let item = schemas[i];

Expand All @@ -102,11 +113,11 @@ export default class Realm {
}
}

let realmId = rpc.createRealm(Array.from(arguments));
setupRealm(this, realmId);
let info = rpc.createRealm(Array.from(arguments));
setupRealm(this, info);

// This will create mappings between the id, path, and potential constructors.
objects.registerConstructors(realmId, this.path, constructors);
objects.registerConstructors(info.realmId, this.path, constructors);
}

create(type, ...args) {
Expand All @@ -130,7 +141,6 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [
'addListener',
'removeListener',
'removeAllListeners',
'close',
'privileges',
'writeCopyTo',
'_waitForDownload',
Expand All @@ -144,6 +154,7 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [
'deleteAll',
'write',
'compact',
'close',
'beginTransaction',
'commitTransaction',
'cancelTransaction',
Expand Down Expand Up @@ -175,7 +186,7 @@ Object.defineProperties(Realm, {
value: Sync,
},
defaultPath: {
get: util.getterForProperty('defaultPath'),
get: util.getterForProperty('defaultPath', false),
set: util.setterForProperty('defaultPath'),
},
schemaVersion: {
Expand All @@ -195,14 +206,14 @@ Object.defineProperties(Realm, {
},
clearTestState: {
value: function() {
collections.clearMutationListeners();
objects.clearRegisteredConstructors();
util.invalidateCache();
rpc.clearTestState();
},
},
_asyncOpen: {
value: function() {
return rpc.callMethod(undefined, Realm[keys.id], '_asyncOpen', Array.from(arguments));
value: function(config, callback) {
return rpc.asyncOpenRealm(Realm[keys.id], config, callback);
},
},
});
Expand Down
7 changes: 6 additions & 1 deletion lib/browser/objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
'use strict';

import { keys, objectTypes } from './constants';
import { getterForProperty, setterForProperty, createMethods } from './util';
import { getterForProperty, setterForProperty, createMethods, cacheObject } from './util';
import * as rpc from './rpc'

let registeredConstructors = {};
let registeredRealmPaths = {};
Expand Down Expand Up @@ -69,6 +70,10 @@ export function createObject(realmId, info) {
throw new Error('Realm object constructor must not return another value');
}
}
for (let key in info.cache) {
info.cache[key] = rpc.deserialize(undefined, info.cache[key])
}
cacheObject(realmId, info.id, info.cache);

return object;
}
Expand Down
1 change: 1 addition & 0 deletions lib/browser/results.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default class Results extends Collection {

// Non-mutating methods:
createMethods(Results.prototype, objectTypes.RESULTS, [
'description',
'filtered',
'sorted',
'snapshot',
Expand Down
58 changes: 55 additions & 3 deletions lib/browser/rpc.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'use strict';

import * as base64 from './base64';
import * as util from './util';
import { keys, objectTypes } from './constants';

const { id: idKey, realm: _realmKey } = keys;
Expand Down Expand Up @@ -57,12 +58,41 @@ export function createSession(refreshAccessToken, host) {
return sessionId;
}

function beforeNotify(realm) {
// NOTE: the mere existence of this function is important for read
// isolation even independent of what it does in its body. By having a
// beforenotify listener, we ensure that the RPC server can't proceed in
// notify() to autorefresh until the browser performs a callback poll.
// Without this, the RPC server could autorefresh in between two subsequent
// property reads from the browser.

// Clear the cache for this Realm, and reenable caching if it was disabled
// by a write transaction.
util.invalidateCache(realm[keys.realm]);
}

export function createRealm(args) {
if (args) {
args = args.map((arg) => serialize(null, arg));
}

return sendRequest('create_realm', { arguments: args });
return sendRequest('create_realm', { arguments: args, beforeNotify: serialize(null, beforeNotify) });
}

export function asyncOpenRealm(id, config, callback) {
sendRequest('call_method', {
id,
name: '_asyncOpen',
arguments: [
serialize(null, config),
serialize(null, (realm, error) => {
if (realm) {
realm.addListener('beforenotify', beforeNotify);
}
callback(realm, error);
})
]
});
}

export function createUser(args) {
Expand Down Expand Up @@ -105,6 +135,17 @@ export function callMethod(realmId, id, name, args) {
return deserialize(realmId, result);
}

export function getObject(realmId, id, name) {
let result = sendRequest('get_object', { realmId, id, name });
if (!result) {
return result;
}
for (let key in result) {
result[key] = deserialize(realmId, result[key]);
}
return result;
}

export function getProperty(realmId, id, name) {
let result = sendRequest('get_property', { realmId, id, name });
return deserialize(realmId, result);
Expand Down Expand Up @@ -231,6 +272,7 @@ function makeRequest(url, data) {
}

let pollTimeoutId;
let pollTimeout = 10;

//returns an object from rpc serialized json value
function deserialize_json_value(value) {
Expand Down Expand Up @@ -260,6 +302,17 @@ function sendRequest(command, data, host = sessionHost) {

let url = 'http://' + host + '/' + command;
let response = makeRequest(url, data);
let callback = response && response.callback;

// Reset the callback poll interval to 10ms every time we either hit a
// callback or call any other method, and double it each time we poll
// for callbacks and get nothing until it's over a second.
if (callback || command !== 'callbacks_poll') {
pollTimeout = 10;
}
else if (pollTimeout < 1000) {
pollTimeout *= 2;
}

if (!response || response.error) {
let error = response && response.error;
Expand All @@ -283,7 +336,6 @@ function sendRequest(command, data, host = sessionHost) {

throw new Error(error || `Invalid response for "${command}"`);
}
let callback = response.callback;
if (callback != null) {
let result, error, stack;
try {
Expand Down Expand Up @@ -315,6 +367,6 @@ function sendRequest(command, data, host = sessionHost) {
return response.result;
}
finally {
pollTimeoutId = setTimeout(() => sendRequest('callbacks_poll'), 100);
pollTimeoutId = setTimeout(() => sendRequest('callbacks_poll'), pollTimeout);
}
}
6 changes: 3 additions & 3 deletions lib/browser/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ export default class Session {
}

Object.defineProperties(Session.prototype, {
connectionState: { get: getterForProperty('connectionState') },
state: { get: getterForProperty('state') },
url: { get: getterForProperty('url') },
connectionState: { get: getterForProperty('connectionState', false) },
state: { get: getterForProperty('state', false) },
url: { get: getterForProperty('url', false) },
});

createMethods(Session.prototype, objectTypes.SESSION, [
Expand Down
Loading