Skip to content

Commit 7bdc4d3

Browse files
authored
build: Release (#10121)
2 parents 9c9a40d + 3f5381d commit 7bdc4d3

42 files changed

Lines changed: 1911 additions & 211 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

DEPRECATIONS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
1818
| DEPPS12 | Database option `allowPublicExplain` defaults to `false` | [#7519](https://github.com/parse-community/parse-server/issues/7519) | 8.5.0 (2025) | 9.0.0 (2026) | changed | - |
1919
| DEPPS13 | Config option `enableInsecureAuthAdapters` defaults to `false` | [#9667](https://github.com/parse-community/parse-server/pull/9667) | 8.0.0 (2025) | 9.0.0 (2026) | changed | - |
2020
| DEPPS14 | Config option `pages.encodePageParamHeaders` defaults to `true` | [#10063](https://github.com/parse-community/parse-server/issues/10063) | 9.4.0 (2026) | 10.0.0 (2027) | deprecated | - |
21+
| DEPPS15 | Config option `readOnlyMasterKeyIps` defaults to `['127.0.0.1', '::1']` | [#10115](https://github.com/parse-community/parse-server/pull/10115) | 9.5.0 (2026) | 10.0.0 (2027) | deprecated | - |
22+
| DEPPS16 | Remove config option `mountPlayground` | [#10110](https://github.com/parse-community/parse-server/issues/10110) | 9.5.0 (2026) | 10.0.0 (2027) | deprecated | - |
23+
| DEPPS17 | Remove config option `playgroundPath` | [#10110](https://github.com/parse-community/parse-server/issues/10110) | 9.5.0 (2026) | 10.0.0 (2027) | deprecated | - |
2124

2225
[i_deprecation]: ## "The version and date of the deprecation."
2326
[i_change]: ## "The version and date of the planned change."

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
305305

306306
<sub>(1) `Parse.Object.createdAt`, `Parse.Object.updatedAt`.</sub>
307307

308+
> [!NOTE]
309+
> In Cloud Code, both `masterKey` and `readOnlyMasterKey` set `request.master` to `true`. To distinguish between them, check `request.isReadOnly`. For example, use `request.master && !request.isReadOnly` to ensure full master key access.
310+
308311
## Email Verification and Password Reset
309312

310313
Verifying user email addresses and enabling password reset via email requires an email adapter. There are many email adapters provided and maintained by the community. The following is an example configuration with an example email adapter. See the [Parse Server Options][server-options] for more details and a full list of available options.
@@ -819,7 +822,7 @@ $ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongo
819822
820823
After starting the server, you can visit http://localhost:1337/playground in your browser to start playing with your GraphQL API.
821824
822-
**_Note:_** Do **_NOT_** use --mountPlayground option in production. [Parse Dashboard](https://github.com/parse-community/parse-dashboard) has a built-in GraphQL Playground and it is the recommended option for production apps.
825+
**_Note:_** Do **_NOT_** use --mountPlayground option in production. The GraphQL Playground exposes the master key in the browser page. [Parse Dashboard](https://github.com/parse-community/parse-dashboard) has a built-in GraphQL Playground and is the recommended option for production apps.
823826
824827
### Using Docker
825828
@@ -842,7 +845,7 @@ $ docker run --name my-parse-server --link my-mongo:mongo -v config-vol:/parse-s
842845
843846
After starting the server, you can visit http://localhost:1337/playground in your browser to start playing with your GraphQL API.
844847
845-
**_Note:_** Do **_NOT_** use --mountPlayground option in production. [Parse Dashboard](https://github.com/parse-community/parse-dashboard) has a built-in GraphQL Playground and it is the recommended option for production apps.
848+
**_Note:_** Do **_NOT_** use --mountPlayground option in production. The GraphQL Playground exposes the master key in the browser page. [Parse Dashboard](https://github.com/parse-community/parse-dashboard) has a built-in GraphQL Playground and is the recommended option for production apps.
846849
847850
### Using Express.js
848851
@@ -896,7 +899,7 @@ $ node index.js
896899
897900
After starting the app, you can visit http://localhost:1337/playground in your browser to start playing with your GraphQL API.
898901
899-
**_Note:_** Do **_NOT_** mount the GraphQL Playground in production. [Parse Dashboard](https://github.com/parse-community/parse-dashboard) has a built-in GraphQL Playground and it is the recommended option for production apps.
902+
**_Note:_** Do **_NOT_** mount the GraphQL Playground in production. The GraphQL Playground exposes the master key in the browser page. [Parse Dashboard](https://github.com/parse-community/parse-dashboard) has a built-in GraphQL Playground and is the recommended option for production apps.
900903
901904
## Checking the API health
902905

benchmark/performance.js

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const LOG_ITERATIONS = false;
2323

2424
// Parse Server instance
2525
let parseServer;
26+
let httpServer;
2627
let mongoClient;
2728
let core;
2829

@@ -48,6 +49,7 @@ async function initializeParseServer() {
4849
allowClientClassCreation: true,
4950
logLevel: 'error', // Minimal logging for performance
5051
verbose: false,
52+
liveQuery: { classNames: ['BenchmarkLiveQuery'] },
5153
});
5254

5355
app.use('/parse', parseServer.app);
@@ -195,6 +197,105 @@ async function measureOperation({ name, operation, iterations, skipWarmup = fals
195197
}
196198
}
197199

200+
/**
201+
* Measure GC pressure for an async operation over multiple iterations.
202+
* Tracks garbage collection duration per operation using PerformanceObserver.
203+
* Larger transient allocations (e.g., from unbounded cursor batch sizes) cause
204+
* more frequent and longer GC pauses, which this metric directly captures.
205+
* @param {Object} options Measurement options.
206+
* @param {string} options.name Name of the operation being measured.
207+
* @param {Function} options.operation Async function to measure.
208+
* @param {number} options.iterations Number of iterations to run.
209+
* @param {boolean} [options.skipWarmup=false] Skip warmup phase.
210+
*/
211+
async function measureMemoryOperation({ name, operation, iterations, skipWarmup = false }) {
212+
const { PerformanceObserver } = require('node:perf_hooks');
213+
214+
// Override iterations if global ITERATIONS is set
215+
iterations = ITERATIONS || iterations;
216+
217+
// Determine warmup count (20% of iterations)
218+
const warmupCount = skipWarmup ? 0 : Math.floor(iterations * 0.2);
219+
const gcDurations = [];
220+
221+
if (warmupCount > 0) {
222+
logInfo(`Starting warmup phase of ${warmupCount} iterations...`);
223+
for (let i = 0; i < warmupCount; i++) {
224+
await operation();
225+
}
226+
logInfo('Warmup complete.');
227+
}
228+
229+
// Measurement phase
230+
logInfo(`Starting measurement phase of ${iterations} iterations...`);
231+
const progressInterval = Math.ceil(iterations / 10);
232+
233+
for (let i = 0; i < iterations; i++) {
234+
// Force GC before each iteration to start from a clean state
235+
if (typeof global.gc === 'function') {
236+
global.gc();
237+
}
238+
239+
// Track GC events during this iteration; measure the longest single GC pause,
240+
// which reflects the production impact of large transient allocations
241+
let maxGcPause = 0;
242+
const obs = new PerformanceObserver((list) => {
243+
for (const entry of list.getEntries()) {
244+
if (entry.duration > maxGcPause) {
245+
maxGcPause = entry.duration;
246+
}
247+
}
248+
});
249+
obs.observe({ type: 'gc', buffered: false });
250+
251+
await operation();
252+
253+
// Flush any buffered entries before disconnecting to avoid data loss
254+
for (const entry of obs.takeRecords()) {
255+
if (entry.duration > maxGcPause) {
256+
maxGcPause = entry.duration;
257+
}
258+
}
259+
obs.disconnect();
260+
gcDurations.push(maxGcPause);
261+
262+
if (LOG_ITERATIONS) {
263+
logInfo(`Iteration ${i + 1}: ${maxGcPause.toFixed(2)} ms GC`);
264+
} else if ((i + 1) % progressInterval === 0 || i + 1 === iterations) {
265+
const progress = Math.round(((i + 1) / iterations) * 100);
266+
logInfo(`Progress: ${progress}%`);
267+
}
268+
}
269+
270+
// Sort for percentile calculations
271+
gcDurations.sort((a, b) => a - b);
272+
273+
// Filter outliers using IQR method
274+
const q1Index = Math.floor(gcDurations.length * 0.25);
275+
const q3Index = Math.floor(gcDurations.length * 0.75);
276+
const q1 = gcDurations[q1Index];
277+
const q3 = gcDurations[q3Index];
278+
const iqr = q3 - q1;
279+
const lowerBound = q1 - 1.5 * iqr;
280+
const upperBound = q3 + 1.5 * iqr;
281+
282+
const filtered = gcDurations.filter(d => d >= lowerBound && d <= upperBound);
283+
284+
const median = filtered[Math.floor(filtered.length * 0.5)];
285+
const p95 = filtered[Math.floor(filtered.length * 0.95)];
286+
const p99 = filtered[Math.floor(filtered.length * 0.99)];
287+
const min = filtered[0];
288+
const max = filtered[filtered.length - 1];
289+
290+
return {
291+
name,
292+
value: median,
293+
unit: 'ms',
294+
range: `${min.toFixed(2)} - ${max.toFixed(2)}`,
295+
extra: `p95: ${p95.toFixed(2)}ms, p99: ${p99.toFixed(2)}ms, n=${filtered.length}/${gcDurations.length}`,
296+
};
297+
}
298+
198299
/**
199300
* Benchmark: Object Create
200301
*/
@@ -525,6 +626,171 @@ async function benchmarkQueryWithIncludeNested(name) {
525626
});
526627
}
527628

629+
/**
630+
* Benchmark: Large Result Set GC Pressure
631+
* Measures max GC pause when querying many large documents, which is affected
632+
* by MongoDB cursor batch size configuration. Without a batch size limit,
633+
* the driver processes larger data chunks between yield points, creating more
634+
* garbage that triggers longer GC pauses.
635+
*/
636+
async function benchmarkLargeResultMemory(name) {
637+
const TestObject = Parse.Object.extend('BenchmarkLargeResult');
638+
const TOTAL_OBJECTS = 3_000;
639+
const SAVE_BATCH_SIZE = 200;
640+
641+
// Seed data in batches; ~8 KB per document so 3,000 docs ≈ 24 MB total,
642+
// exceeding MongoDB's 16 MiB default batch limit to test cursor batching
643+
for (let i = 0; i < TOTAL_OBJECTS; i += SAVE_BATCH_SIZE) {
644+
const batch = [];
645+
for (let j = 0; j < SAVE_BATCH_SIZE && i + j < TOTAL_OBJECTS; j++) {
646+
const obj = new TestObject();
647+
obj.set('category', (i + j) % 10);
648+
obj.set('value', i + j);
649+
obj.set('data', `padding-${i + j}-${'x'.repeat(8000)}`);
650+
batch.push(obj);
651+
}
652+
await Parse.Object.saveAll(batch);
653+
}
654+
655+
return measureMemoryOperation({
656+
name,
657+
iterations: 100,
658+
operation: async () => {
659+
const query = new Parse.Query('BenchmarkLargeResult');
660+
query.limit(TOTAL_OBJECTS);
661+
await query.find({ useMasterKey: true });
662+
},
663+
});
664+
}
665+
666+
/**
667+
* Benchmark: Concurrent Query GC Pressure
668+
* Measures max GC pause under concurrent load with large result sets.
669+
* Simulates production conditions where multiple clients query simultaneously,
670+
* compounding GC pressure from cursor batch sizes.
671+
*/
672+
async function benchmarkConcurrentQueryMemory(name) {
673+
const TestObject = Parse.Object.extend('BenchmarkConcurrentResult');
674+
const TOTAL_OBJECTS = 3_000;
675+
const SAVE_BATCH_SIZE = 200;
676+
const CONCURRENT_QUERIES = 10;
677+
678+
// Seed data in batches; ~8 KB per document so 3,000 docs ≈ 24 MB total,
679+
// exceeding MongoDB's 16 MiB default batch limit to test cursor batching
680+
for (let i = 0; i < TOTAL_OBJECTS; i += SAVE_BATCH_SIZE) {
681+
const batch = [];
682+
for (let j = 0; j < SAVE_BATCH_SIZE && i + j < TOTAL_OBJECTS; j++) {
683+
const obj = new TestObject();
684+
obj.set('category', (i + j) % 10);
685+
obj.set('value', i + j);
686+
obj.set('data', `padding-${i + j}-${'x'.repeat(8000)}`);
687+
batch.push(obj);
688+
}
689+
await Parse.Object.saveAll(batch);
690+
}
691+
692+
return measureMemoryOperation({
693+
name,
694+
iterations: 50,
695+
operation: async () => {
696+
const queries = [];
697+
for (let i = 0; i < CONCURRENT_QUERIES; i++) {
698+
const query = new Parse.Query('BenchmarkConcurrentResult');
699+
query.limit(TOTAL_OBJECTS);
700+
queries.push(query.find({ useMasterKey: true }));
701+
}
702+
await Promise.all(queries);
703+
},
704+
});
705+
}
706+
707+
/**
708+
* Benchmark: Query $regex
709+
*
710+
* Measures a standard Parse.Query.find() with a $regex constraint.
711+
* Each iteration uses a different regex to avoid database query cache hits.
712+
*/
713+
async function benchmarkQueryRegex(name) {
714+
// Seed objects that will match the various regex patterns
715+
const objects = [];
716+
for (let i = 0; i < 1_000; i++) {
717+
const obj = new Parse.Object('BenchmarkRegex');
718+
obj.set('field', `BenchRegex_${i} data`);
719+
objects.push(obj);
720+
}
721+
await Parse.Object.saveAll(objects);
722+
723+
let counter = 0;
724+
725+
const bases = ['^BenchRegex_', 'BenchRegex_', '[a-z]+_'];
726+
727+
return measureOperation({
728+
name,
729+
iterations: 1_000,
730+
operation: async () => {
731+
const idx = counter++;
732+
const regex = bases[idx % bases.length] + idx;
733+
const query = new Parse.Query('BenchmarkRegex');
734+
query._addCondition('field', '$regex', regex);
735+
await query.find();
736+
},
737+
});
738+
}
739+
740+
/**
741+
* Benchmark: LiveQuery $regex end-to-end
742+
*
743+
* Measures the full round-trip of a LiveQuery subscription with a $regex constraint:
744+
* subscribe with a unique regex pattern, save an object that matches, and measure
745+
* the time until the LiveQuery event fires. Each iteration uses a different regex
746+
* to avoid cache hits on the RE2JS compile step.
747+
*/
748+
async function benchmarkLiveQueryRegex(name) {
749+
// Enable LiveQuery on the running server
750+
const { default: ParseServer } = require('../lib/index.js');
751+
const liveQueryServer = await ParseServer.createLiveQueryServer(httpServer, {
752+
appId: APP_ID,
753+
masterKey: MASTER_KEY,
754+
serverURL: SERVER_URL,
755+
});
756+
Parse.liveQueryServerURL = 'ws://localhost:1337';
757+
758+
let counter = 0;
759+
760+
// Cycle through different regex patterns to avoid RE2JS cache hits
761+
const patterns = [
762+
{ base: '^BenchLQ_', fieldValue: i => `BenchLQ_${i} data` },
763+
{ base: 'benchfield_', fieldValue: i => `some benchfield_${i} here` },
764+
{ base: '[a-z]+_benchclass_', fieldValue: i => `abc_benchclass_${i}` },
765+
];
766+
767+
try {
768+
return await measureOperation({
769+
name,
770+
iterations: 500,
771+
operation: async () => {
772+
const idx = counter++;
773+
const pattern = patterns[idx % patterns.length];
774+
const regex = pattern.base + idx;
775+
const query = new Parse.Query('BenchmarkLiveQuery');
776+
query._addCondition('field', '$regex', regex);
777+
const subscription = await query.subscribe();
778+
const eventPromise = new Promise(resolve => {
779+
subscription.on('create', () => resolve());
780+
});
781+
const obj = new Parse.Object('BenchmarkLiveQuery');
782+
obj.set('field', pattern.fieldValue(idx));
783+
await obj.save();
784+
await eventPromise;
785+
subscription.unsubscribe();
786+
},
787+
});
788+
} finally {
789+
await liveQueryServer.shutdown();
790+
Parse.liveQueryServerURL = undefined;
791+
}
792+
}
793+
528794
/**
529795
* Run all benchmarks
530796
*/
@@ -538,6 +804,7 @@ async function runBenchmarks() {
538804
// Initialize Parse Server
539805
logInfo('Initializing Parse Server...');
540806
server = await initializeParseServer();
807+
httpServer = server;
541808

542809
// Wait for server to be ready
543810
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -555,6 +822,10 @@ async function runBenchmarks() {
555822
{ name: 'User.login', fn: benchmarkUserLogin },
556823
{ name: 'Query.include (parallel pointers)', fn: benchmarkQueryWithIncludeParallel },
557824
{ name: 'Query.include (nested pointers)', fn: benchmarkQueryWithIncludeNested },
825+
{ name: 'Query.find (large result, GC pressure)', fn: benchmarkLargeResultMemory },
826+
{ name: 'Query.find (concurrent, GC pressure)', fn: benchmarkConcurrentQueryMemory },
827+
{ name: 'Query $regex', fn: benchmarkQueryRegex },
828+
{ name: 'LiveQuery $regex', fn: benchmarkLiveQueryRegex },
558829
];
559830

560831
// Run each benchmark with database cleanup

0 commit comments

Comments
 (0)