Skip to content

wip: Performance improvements #8416

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

Closed
Closed
Show file tree
Hide file tree
Changes from all 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 spec/SchemaPerformance.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,23 @@ describe('Schema Performance', function () {
);
expect(getAllSpy.calls.count()).toBe(2);
});

fit('can save objects', async done => {
const start_parellel = Date.now();
await Parse.User.signUp('username', 'password');

const objects = [];
for (let i = 0; i < 1000; i++) {
const obj = new Parse.Object('TestObj');
obj.set('field1', 'uuid()');
objects.push(obj);
// if ( i == 0) {
// await obj.save();
// }
}
await Promise.all(objects.map(o => o.save()));
const end_parellel = Date.now() - start_parellel;
console.log(end_parellel);
expect(end_parellel).toBeLessThan(6000);
});
});
29 changes: 20 additions & 9 deletions src/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { isDeepStrictEqual } from 'util';
import { getRequestObject, resolveError } from './triggers';
import Deprecator from './Deprecator/Deprecator';
import { logger } from './logger';
const authPromisesByUser = {};

// An Auth object tells you who is requesting something and whether
// the master key was used.
Expand Down Expand Up @@ -174,7 +175,9 @@ Auth.prototype.getUserRoles = function () {

Auth.prototype.getRolesForUser = async function () {
//Stack all Parse.Role
const results = [];
if (authPromisesByUser[this.user.id]) {
return authPromisesByUser[this.user.id]
}
if (this.config) {
const restWhere = {
users: {
Expand All @@ -184,15 +187,23 @@ Auth.prototype.getRolesForUser = async function () {
},
};
const RestQuery = require('./RestQuery');
await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
results.push(result)
);
} else {
await new Parse.Query(Parse.Role)
.equalTo('users', this.user)
.each(result => results.push(result.toJSON()), { useMasterKey: true });
const findObjects = async () => {
const results = [];
await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
results.push(result)
);
return results;
}
authPromisesByUser[this.user.id] = findObjects();
return authPromisesByUser[this.user.id];
}
return results;
authPromisesByUser[this.user.id] = new Parse.Query(Parse.Role)
.equalTo('users', this.user)
.findAll({ useMasterKey: true }).then(response => {
delete authPromisesByUser[this.user.id];
return response;
});
return authPromisesByUser[this.user.id];
};

// Iterates through the role tree and compiles a user's roles
Expand Down
7 changes: 1 addition & 6 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import { isBoolean, isString } from 'lodash';
import net from 'net';
import AppCache from './cache';
import DatabaseController from './Controllers/DatabaseController';
import { logLevels as validLogLevels } from './Controllers/LoggerController';
import {
AccountLockoutOptions,
Expand Down Expand Up @@ -37,11 +36,7 @@ export class Config {
const config = new Config();
config.applicationId = applicationId;
Object.keys(cacheInfo).forEach(key => {
if (key == 'databaseController') {
config.database = new DatabaseController(cacheInfo.databaseController.adapter, config);
} else {
config[key] = cacheInfo[key];
}
config[key] = cacheInfo[key];
});
config.mount = removeTrailingSlash(mount);
config.generateSessionExpiresAt = config.generateSessionExpiresAt.bind(config);
Expand Down
8 changes: 3 additions & 5 deletions src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,15 +407,13 @@ class DatabaseController {
loadSchema(
options: LoadSchemaOptions = { clearCache: false }
): Promise<SchemaController.SchemaController> {
if (this.schemaPromise != null) {
if (this.schemaPromise) {
return this.schemaPromise;
}
this.schemaPromise = SchemaController.load(this.adapter, options);
this.schemaPromise.then(
() => delete this.schemaPromise,
this.schemaPromise = SchemaController.load(this.adapter, options).catch(
() => delete this.schemaPromise
);
return this.loadSchema(options);
return this.schemaPromise;
}

loadSchemaIfNeeded(
Expand Down
109 changes: 81 additions & 28 deletions src/Controllers/SchemaController.js
Original file line number Diff line number Diff line change
Expand Up @@ -687,15 +687,21 @@ const typeToString = (type: SchemaField | string): string => {
// the mongo format and the Parse format. Soon, this will all be Parse format.
export default class SchemaController {
_dbAdapter: StorageAdapter;
_reloadingData: { [string]: any };
_addFieldPromises: { [string]: any };
_addClassPromises: { [string]: any };
schemaData: { [string]: Schema };
reloadDataPromise: ?Promise<any>;
protectedFields: any;
userIdRegEx: RegExp;

constructor(databaseAdapter: StorageAdapter) {
this._dbAdapter = databaseAdapter;
this.schemaData = new SchemaData(SchemaCache.all(), this.protectedFields);
this.protectedFields = Config.get(Parse.applicationId).protectedFields;
this._reloadingData = {
promise: null,
className: '',
};

const customIds = Config.get(Parse.applicationId).allowCustomObjectId;

Expand All @@ -709,24 +715,39 @@ export default class SchemaController {
});
}

reloadData(options: LoadSchemaOptions = { clearCache: false }): Promise<any> {
if (this.reloadDataPromise && !options.clearCache) {
return this.reloadDataPromise;
reloadData(options: LoadSchemaOptions = { clearCache: false, className: '' }): Promise<any> {
if (options.className) {
if (this._reloadingData.className === options.className) {
options.clearCache = false;
} else {
this._reloadingData.className = options.className;
}
} else {
this._reloadingData.className = '';
}
this.reloadDataPromise = this.getAllClasses(options)
if (this._reloadingData.promise && !options.clearCache) {
return this._reloadingData.promise;
}
this._reloadingData.promise = this.getAllClasses(options)
.then(
allSchemas => {
this.schemaData = new SchemaData(allSchemas, this.protectedFields);
delete this.reloadDataPromise;
this._reloadingData = {
promise: null,
className: '',
};
},
err => {
this.schemaData = new SchemaData();
delete this.reloadDataPromise;
this._reloadingData = {
promise: null,
className: '',
};
throw err;
}
)
.then(() => {});
return this.reloadDataPromise;
return this._reloadingData.promise;
}

getAllClasses(options: LoadSchemaOptions = { clearCache: false }): Promise<Array<Schema>> {
Expand All @@ -741,9 +762,14 @@ export default class SchemaController {
}

setAllClasses(): Promise<Array<Schema>> {
const r = 'classes ' + (Math.random() + 1).toString(36).substring(7);
console.time(r);
return this._dbAdapter
.getAllClasses()
.then(allSchemas => allSchemas.map(injectDefaultSchema))
.then(allSchemas => {
// console.timeEnd(r);
return allSchemas.map(injectDefaultSchema);
})
.then(allSchemas => {
SchemaCache.put(allSchemas);
return allSchemas;
Expand Down Expand Up @@ -787,7 +813,7 @@ export default class SchemaController {
// on success, and rejects with an error on fail. Ensure you
// have authorization (master key, or client class creation
// enabled) before calling this function.
async addClassIfNotExists(
addClassIfNotExists(
className: string,
fields: SchemaFields = {},
classLevelPermissions: any,
Expand All @@ -802,27 +828,39 @@ export default class SchemaController {
}
return Promise.reject(validationError);
}
try {
const adapterSchema = await this._dbAdapter.createClass(
if (!this._addClassPromises) {
this._addClassPromises = {};
}
if (this._addClassPromises[className]) {
return this._addClassPromises[className];
}
this._addClassPromises[className] = this._dbAdapter
.createClass(
className,
convertSchemaToAdapterSchema({
fields,
classLevelPermissions,
indexes,
className,
})
);
// TODO: Remove by updating schema cache directly
await this.reloadData({ clearCache: true });
const parseSchema = convertAdapterSchemaToParseSchema(adapterSchema);
return parseSchema;
} catch (error) {
if (error && error.code === Parse.Error.DUPLICATE_VALUE) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
} else {
throw error;
}
}
)
.then(async adapterSchema => {
await this.reloadData({ clearCache: true });
const parseSchema = convertAdapterSchemaToParseSchema(adapterSchema);
delete this._addClassPromises[className];
return parseSchema;
})
.catch(error => {
if (error && error.code === Parse.Error.DUPLICATE_VALUE) {
throw new Parse.Error(
Parse.Error.INVALID_CLASS_NAME,
`Class ${className} already exists.`
);
} else {
throw error;
}
});
return this._addClassPromises[className];
}

updateClass(
Expand Down Expand Up @@ -947,7 +985,7 @@ export default class SchemaController {
// have failed because there's a race condition and a different
// client is making the exact same schema update that we want.
// So just reload the schema.
return this.reloadData({ clearCache: true });
return this.reloadData({ clearCache: true, className });
})
.then(() => {
// Ensure that the schema now validates
Expand All @@ -957,7 +995,7 @@ export default class SchemaController {
throw new Parse.Error(Parse.Error.INVALID_JSON, `Failed to add ${className}`);
}
})
.catch(() => {
.catch(e => {
// The schema still doesn't validate. Give up
throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate');
})
Expand Down Expand Up @@ -1131,9 +1169,22 @@ export default class SchemaController {
return this._dbAdapter.updateFieldOptions(className, fieldName, type);
}

return this._dbAdapter
if (!this._addFieldPromises) {
this._addFieldPromises = {};
}

if (!this._addFieldPromises[className]) {
this._addFieldPromises[className] = {};
}

if (this._addFieldPromises[className][`${fieldName}-${type}`]) {
return this._addFieldPromises[className][`${fieldName}-${type}`];
}

this._addFieldPromises[className][`${fieldName}-${type}`] = this._dbAdapter
.addFieldIfNotExists(className, fieldName, type)
.catch(error => {
delete this._addFieldPromises[className][`${fieldName}-${type}`];
if (error.code == Parse.Error.INCORRECT_TYPE) {
// Make sure that we throw errors when it is appropriate to do so.
throw error;
Expand All @@ -1144,12 +1195,14 @@ export default class SchemaController {
return Promise.resolve();
})
.then(() => {
delete this._addFieldPromises[className][`${fieldName}-${type}`];
return {
className,
fieldName,
type,
};
});
return this._addFieldPromises[className][`${fieldName}-${type}`];
}

ensureFields(fields: any) {
Expand Down Expand Up @@ -1270,7 +1323,7 @@ export default class SchemaController {

if (enforceFields.length !== 0) {
// TODO: Remove by updating schema cache directly
await this.reloadData({ clearCache: true });
await this.reloadData({ clearCache: true, className });
}
this.ensureFields(enforceFields);

Expand Down
1 change: 1 addition & 0 deletions src/Controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export function getControllers(options: ParseServerOptions) {
parseGraphQLController,
liveQueryController,
databaseController,
database: databaseController,
hooksController,
authDataManager,
schemaCache: SchemaCache,
Expand Down