Do not clone model requests on server if possible (#6353)

This commit is contained in:
Andrey Sobolev 2024-08-20 19:39:12 +07:00 committed by GitHub
parent 895a2b1bf6
commit 84cd3b6cbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 101 additions and 30 deletions

View File

@ -189,6 +189,7 @@ export abstract class MemDb extends TxProcessor implements Storage {
/**
* Only in model find without lookups and sorting.
* Do not clone results, so be aware modifications are not allowed.
*/
findAllSync<T extends Doc>(_class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T>): FindResult<T> {
let result: WithLookup<Doc>[]
@ -210,9 +211,13 @@ export abstract class MemDb extends TxProcessor implements Storage {
}
const total = result.length
result = result.slice(0, options?.limit)
const tresult = this.hierarchy.clone(result) as WithLookup<T>[]
const res = tresult.map((it) => this.hierarchy.updateLookupMixin(_class, it, options))
return toFindResult(res, total)
return toFindResult(
result.map((it) => {
return baseClass !== _class ? this.hierarchy.as(it, _class) : it
}) as WithLookup<T>[],
total
)
}
addDoc (doc: Doc): void {

View File

@ -1,3 +1,4 @@
import { PlatformError, unknownError } from '@hcengineering/platform'
import { Ref } from '.'
import type { Doc, Mixin } from './classes'
@ -26,6 +27,46 @@ export function _createMixinProxy (mixin: Mixin<Doc>, ancestorProxy: ProxyHandle
}
}
export function freeze (value: any): any {
if (value != null && typeof value === 'object') {
if (Array.isArray(value)) {
return value.map((it) => freeze(it))
}
if (value instanceof Map) {
throw new PlatformError(unknownError('Map is not allowed in model'))
}
if (value instanceof Set) {
throw new PlatformError(unknownError('Set is not allowed in model'))
}
return new Proxy(value, _createFreezeProxy(value))
}
return value
}
/**
* @internal
*/
export function _createFreezeProxy (doc: Doc): ProxyHandler<Doc> {
return {
get (target: any, property: string, receiver: any): any {
const value = target[property]
return freeze(value)
},
set (target, p, newValue, receiver): any {
throw new PlatformError(unknownError('Modification is not allowed'))
},
defineProperty (target, property, attributes): any {
throw new PlatformError(unknownError('Modification is not allowed'))
},
deleteProperty (target, p): any {
throw new PlatformError(unknownError('Modification is not allowed'))
},
setPrototypeOf (target, v): any {
throw new PlatformError(unknownError('Modification is not allowed'))
}
}
}
/**
* @internal
*/

View File

@ -111,6 +111,9 @@ export function escapeLikeForRegexp (value: string): string {
*/
export function toFindResult<T extends Doc> (docs: T[], total?: number, lookupMap?: Record<string, Doc>): FindResult<T> {
const length = total ?? docs.length
if (Object.keys(lookupMap ?? {}).length === 0) {
lookupMap = undefined
}
return Object.assign(docs, { total: length, lookupMap })
}

View File

@ -352,10 +352,14 @@
attr: client.getHierarchy().getAttribute(tracker.class.Issue, 'labels')
}
$: spaceQuery.query(tracker.class.Project, { _id: _space }, (res) => {
resetDefaultAssigneeId()
currentProject = res[0]
})
$: if (_space !== undefined) {
spaceQuery.query(tracker.class.Project, { _id: _space }, (res) => {
resetDefaultAssigneeId()
currentProject = res[0]
})
} else {
currentProject = undefined
}
const docCreateManager = DocCreateExtensionManager.create(tracker.class.Issue)
@ -758,7 +762,7 @@
label={tracker.string.Project}
bind:space={_space}
on:object={(evt) => {
currentProject = evt.detail
currentProject = evt.detail ?? undefined
}}
kind={'regular'}
size={'small'}

View File

@ -1,7 +1,7 @@
FROM node:20
WORKDIR /usr/src/app
RUN npm install --ignore-scripts=false --verbose bufferutil utf-8-validate @mongodb-js/zstd snappy --unsafe-perm
RUN npm install --ignore-scripts=false --verbose bufferutil utf-8-validate @mongodb-js/zstd snappy msgpackr msgpackr-extract --unsafe-perm
RUN npm install --ignore-scripts=false --verbose uNetworking/uWebSockets.js#v20.47.0
RUN apt-get update
@ -16,4 +16,4 @@ COPY bundle/bundle.js.map ./
EXPOSE 8080
ENV UWS_HTTP_MAX_HEADERS_SIZE 32768
CMD node --enable-source-maps --inspect bundle.js
CMD node --enable-source-maps --inspect=0.0.0.0:9229 bundle.js

View File

@ -15,7 +15,7 @@
"_phase:bundle": "rushx bundle",
"_phase:docker-build": "rushx docker:build",
"_phase:docker-staging": "rushx docker:staging",
"bundle": "mkdir -p bundle && esbuild src/__start.ts --sourcemap=inline --bundle --keep-names --platform=node --external:*.node --external:bufferutil --external:snappy --external:utf-8-validate --define:process.env.MODEL_VERSION=$(node ../../common/scripts/show_version.js) --define:process.env.GIT_REVISION=$(../../common/scripts/git_version.sh) --outfile=bundle/bundle.js --log-level=error --sourcemap=external",
"bundle": "mkdir -p bundle && esbuild src/__start.ts --sourcemap=inline --bundle --keep-names --platform=node --external:*.node --external:bufferutil --external:snappy --external:utf-8-validate --external:msgpackr-extract --define:process.env.MODEL_VERSION=$(node ../../common/scripts/show_version.js) --define:process.env.GIT_REVISION=$(../../common/scripts/git_version.sh) --outfile=bundle/bundle.js --log-level=error --sourcemap=external",
"docker:build": "../../common/scripts/docker_build.sh hardcoreeng/transactor",
"docker:tbuild": "docker build -t hardcoreeng/transactor . --platform=linux/amd64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/transactor",
"docker:abuild": "docker build -t hardcoreeng/transactor . --platform=linux/arm64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/transactor",

View File

@ -71,7 +71,8 @@ export class LookupMiddleware extends BaseMiddleware implements Middleware {
for (const d of result) {
if (d.$lookup !== undefined) {
const newDoc = clone(d)
const newDoc: any = { ...d }
newDoc.$lookup = clone(d.$lookup)
newResult.push(newDoc)
for (const [k, v] of Object.entries(d.$lookup)) {
if (!Array.isArray(v)) {
@ -83,22 +84,36 @@ export class LookupMiddleware extends BaseMiddleware implements Middleware {
}
}
const lookupMap = Object.fromEntries(Array.from(Object.values(idClassMap)).map((it) => [it.id, it.doc]))
if (Object.keys(lookupMap).length > 0) {
return toFindResult(newResult, result.total, lookupMap)
}
return this.cleanQuery<T>(toFindResult(newResult, result.total, lookupMap), query, lookupMap)
}
// We need to get rid of simple query parameters matched in documents
return this.cleanQuery<T>(result, query)
}
private cleanQuery<T extends Doc>(
result: FindResult<T>,
query: DocumentQuery<T>,
lookupMap?: Record<string, Doc>
): FindResult<T> {
const newResult: T[] = []
for (const doc of result) {
let _doc = doc
let cloned = false
for (const [k, v] of Object.entries(query)) {
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
if ((doc as any)[k] === v) {
if ((_doc as any)[k] === v) {
if (!cloned) {
_doc = { ...doc } as any
cloned = true
}
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (doc as any)[k]
delete (_doc as any)[k]
}
}
}
newResult.push(_doc)
}
return result
return toFindResult(newResult, result.total, lookupMap)
}
}

View File

@ -89,7 +89,7 @@ export interface Response<R> {
}
export class RPCHandler {
packr = new Packr({ structuredClone: true, bundleStrings: true, copyBuffers: true })
packr = new Packr({ structuredClone: true, bundleStrings: true, copyBuffers: false })
protoSerialize (object: object, binary: boolean): any {
if (!binary) {
return JSON.stringify(object, replacer)

View File

@ -4,8 +4,6 @@ FROM node:20
WORKDIR /usr/src/app
RUN npm install --ignore-scripts=false --verbose bufferutil utf-8-validate @mongodb-js/zstd snappy --unsafe-perm
COPY bundle/bundle.js ./
COPY bundle/bundle.js.map ./
RUN apt-get update
RUN apt-get install libjemalloc2
@ -13,5 +11,8 @@ RUN apt-get install libjemalloc2
ENV LD_PRELOAD=libjemalloc.so.2
ENV MALLOC_CONF=dirty_decay_ms:1000,narenas:2,background_thread:true
COPY bundle/bundle.js ./
COPY bundle/bundle.js.map ./
EXPOSE 3078
CMD [ "node", "--inspect", "--async-stack-traces", "--enable-source-maps", "bundle.js" ]

View File

@ -16,7 +16,7 @@ RUN apt-get update && \
WORKDIR /usr/src/app
COPY bundle/bundle.js ./
RUN npm install --ignore-scripts=false --verbose bufferutil utf-8-validate @mongodb-js/zstd snappy --unsafe-perm
COPY bundle/bundle.js ./
CMD [ "dumb-init", "node", "bundle.js" ]

View File

@ -1,10 +1,11 @@
import { expect, type Locator } from '@playwright/test'
import path from 'path'
import { createIssue, toTime } from '../../tracker/tracker.utils'
import { attachScreenshot, iterateLocator } from '../../utils'
import { CommonTrackerPage } from './common-tracker-page'
import { NewIssue } from './types'
import { createIssue, toTime } from '../../tracker/tracker.utils'
const retryOptions = { intervals: [1000, 1500, 2500], timeout: 60000 }
export class IssuesPage extends CommonTrackerPage {
modelSelectorAll = (): Locator => this.page.locator('label[data-id="tab-all"]')
issues = (): Locator => this.page.locator('text="Issues"')
@ -492,15 +493,14 @@ export class IssuesPage extends CommonTrackerPage {
}
async searchIssueByName (issueName: string): Promise<void> {
for (let i = 0; i < 5; i++) {
await expect(async () => {
await this.inputSearchIcon().click()
await this.inputSearch().fill(issueName)
const v = await this.inputSearch().inputValue()
if (v === issueName) {
await this.inputSearch().press('Enter')
break
}
}
}).toPass(retryOptions)
}
async openIssueByName (issueName: string): Promise<void> {
@ -557,9 +557,9 @@ export class IssuesPage extends CommonTrackerPage {
}
async checkIssuesCount (issueName: string, count: number, timeout?: number): Promise<void> {
await expect(this.issueAnchorByName(issueName)).toHaveCount(count, {
timeout: timeout !== undefined ? timeout * 1000 : undefined
})
await expect(async () => {
await expect(this.issueAnchorByName(issueName)).toHaveCount(count)
}).toPass(retryOptions)
}
async selectTemplate (templateName: string): Promise<void> {
@ -573,7 +573,9 @@ export class IssuesPage extends CommonTrackerPage {
}
async checkAttachmentsCount (issueName: string, count: string): Promise<void> {
await expect(this.attachmentContentButton(issueName)).toHaveText(count)
await expect(async () => {
await expect(this.attachmentContentButton(issueName)).toHaveText(count)
}).toPass(retryOptions)
}
async addAttachmentToIssue (issueName: string, filePath: string): Promise<void> {