Skip to content

Commit

Permalink
Merge pull request #2373 from realm/tg/rpc-improvements
Browse files Browse the repository at this point in the history
Improve performance and correctness of chrome debugging
  • Loading branch information
tgoyne authored May 16, 2019
2 parents d7e7824 + db8f033 commit cd57977
Show file tree
Hide file tree
Showing 17 changed files with 461 additions and 340 deletions.
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. (Issue: [#491](https://github.com/realm/realm-js/issues/491), PR: [#2373](https://github.com/realm/realm-js/pull/2373)).

### 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), since v2.15.0).
* 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), since v1.0.0).
* 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), since v1.0.0 in various forms).

### Compatibility
* Realm Object Server: 3.21.0 or later.
* 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

0 comments on commit cd57977

Please sign in to comment.