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

UBER-834: Improve list speed #3692

Merged
merged 1 commit into from
Sep 14, 2023
Merged
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
101 changes: 100 additions & 1 deletion models/recruit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ export function createModel (builder: Builder): void {

const applicantViewOptions = (colors: boolean): ViewOptionsModel => {
const model: ViewOptionsModel = {
groupBy: ['status', 'assignee', 'space', 'createdBy', 'modifiedBy'],
groupBy: ['status', 'doneState', 'assignee', 'space', 'createdBy', 'modifiedBy'],
orderBy: [
['status', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
Expand Down Expand Up @@ -784,6 +784,105 @@ export function createModel (builder: Builder): void {
recruit.viewlet.ListApplicant
)

builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: recruit.mixin.Candidate,
descriptor: view.viewlet.List,
config: [
{ key: '', displayProps: { fixed: 'left', key: 'app' } },
{
key: 'title',
props: { kind: 'list', size: 'small', shouldShowName: false }
},
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
{ key: '', displayProps: { grow: true } },
{
key: '$lookup.channels',
label: contact.string.ContactInfo,
sortingKey: ['$lookup.channels.lastMessage', '$lookup.attachedTo.channels'],
props: {
length: 'full',
size: 'small',
kind: 'list'
},
displayProps: { compression: true }
},
{ key: 'modifiedOn', displayProps: { key: 'modified', fixed: 'right', dividerBefore: true } }
],
configOptions: {
strict: true,
hiddenKeys: ['name']
},
viewOptions: {
groupBy: ['createdBy', 'modifiedBy'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending],
['rank', SortingOrder.Ascending]
],
other: [showColorsViewOption]
}
},
recruit.viewlet.ListTalent
)

builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: recruit.mixin.VacancyList,
descriptor: view.viewlet.List,
config: [
{ key: '', displayProps: { fixed: 'left', key: 'app' } },
{
key: '@vacancies',
label: recruit.string.Vacancies,
props: { kind: 'list', size: 'small', shouldShowName: false }
},
{
key: '@applications',
label: recruit.string.Applications,
props: { kind: 'list', size: 'small', shouldShowName: false }
},
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
{
key: '$lookup.channels',
label: contact.string.ContactInfo,
sortingKey: ['$lookup.channels.lastMessage', '$lookup.attachedTo.channels'],
props: {
length: 'full',
size: 'small',
kind: 'list'
},
displayProps: { compression: true }
},
{ key: '', displayProps: { grow: true } },
{
key: '@applications.modifiedOn',
label: core.string.ModifiedDate,
displayProps: { key: 'modified', fixed: 'right', dividerBefore: true }
}
],
configOptions: {
strict: true,
sortable: true,
hiddenKeys: ['name', 'space', 'modifiedOn']
},
viewOptions: {
groupBy: ['createdBy', 'modifiedBy'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending],
['rank', SortingOrder.Ascending]
],
other: [showColorsViewOption]
}
},
recruit.viewlet.ListCompanies
)

builder.createDoc(
view.class.Viewlet,
core.space.Model,
Expand Down
2 changes: 2 additions & 0 deletions models/recruit/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export default mergeIds(recruitId, recruit, {
ApplicantTable: '' as Ref<Viewlet>,
ApplicantKanban: '' as Ref<Viewlet>,
ListApplicant: '' as Ref<Viewlet>,
ListTalent: '' as Ref<Viewlet>,
ListCompanies: '' as Ref<Viewlet>,
TableApplicant: '' as Ref<Viewlet>,
TableApplicantMatch: '' as Ref<Viewlet>,
CalendarReview: '' as Ref<Viewlet>,
Expand Down
44 changes: 44 additions & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,47 @@ export class DocManager {
return this.docs.filter(predicate)
}
}

/**
* @public
*/

export class RateLimitter {
idCounter: number = 0
processingQueue = new Map<string, Promise<void>>()
last: number = 0

queue: (() => Promise<void>)[] = []

constructor (readonly config: () => { rate: number, perSecond?: number }) {}

async exec<T, B extends Record<string, any> = {}>(op: (args?: B) => Promise<T>, args?: B): Promise<T> {
const processingId = `${this.idCounter++}`
const cfg = this.config()

while (this.processingQueue.size > cfg.rate) {
await Promise.race(this.processingQueue.values())
}
try {
const p = op(args)
this.processingQueue.set(processingId, p as Promise<void>)
return await p
} finally {
this.processingQueue.delete(processingId)
}
}

async add<T, B extends Record<string, any> = {}>(op: (args?: B) => Promise<T>, args?: B): Promise<void> {
const cfg = this.config()

if (this.processingQueue.size < cfg.rate) {
void this.exec(op, args)
} else {
await this.exec(op, args)
}
}

async waitProcessing (): Promise<void> {
await await Promise.race(this.processingQueue.values())
}
}
2 changes: 1 addition & 1 deletion packages/ui/src/components/icons/CollapseArrow.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
export let size: 'tiny' | 'small' | 'medium' | 'large'
export let fill: string = 'currentColor'
</script>

Expand Down
77 changes: 39 additions & 38 deletions plugins/recruit-resources/src/components/Organizations.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,12 @@
-->
<script lang="ts">
import { Organization } from '@hcengineering/contact'
import core, { Doc, DocumentQuery, Ref } from '@hcengineering/core'
import core, { Doc, DocumentQuery, Ref, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Applicant, Vacancy } from '@hcengineering/recruit'
import { Button, IconAdd, Label, Loading, SearchEdit, showPopup } from '@hcengineering/ui'
import view, { BuildModelKey, Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
import {
FilterBar,
FilterButton,
TableBrowser,
ViewletSelector,
ViewletSettingButton
} from '@hcengineering/view-resources'
import { Button, Component, IconAdd, Label, Loading, SearchEdit, showPopup } from '@hcengineering/ui'
import view, { BuildModelKey, ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
import { FilterBar, FilterButton, ViewletSelector, ViewletSettingButton } from '@hcengineering/view-resources'
import recruit from '../plugin'
import CreateOrganization from './CreateOrganization.svelte'
import VacancyListApplicationsPopup from './organizations/VacancyListApplicationsPopup.svelte'
Expand Down Expand Up @@ -175,14 +169,17 @@
]
])

let viewlet: Viewlet | undefined
let viewlet: WithLookup<Viewlet> | undefined
let loading = true

let preference: ViewletPreference | undefined
let viewOptions: ViewOptions | undefined

function createConfig (descr: Viewlet, preference: ViewletPreference | undefined): (string | BuildModelKey)[] {
const base = preference?.config ?? descr.config
function createConfig (
descr: Viewlet | undefined,
preference: ViewletPreference | undefined
): (string | BuildModelKey)[] {
const base = preference?.config ?? descr?.config ?? []
const result: (string | BuildModelKey)[] = []
for (const key of base) {
if (typeof key === 'string') {
Expand All @@ -193,13 +190,24 @@
}
return result
}

$: finalConfig = createConfig(viewlet, preference)
</script>

<div class="ac-header full divide">
<div class="ac-header__wrap-title mr-3">
<span class="ac-header__title"><Label label={recruit.string.Organizations} /></span>
</div>
<div class="clear-mins mb-1">
<div class="ac-header-full medium-gap mb-1">
<ViewletSelector
bind:loading
bind:viewlet
bind:preference
viewletQuery={{
attachTo: recruit.mixin.VacancyList,
descriptor: { $in: [view.viewlet.Table, view.viewlet.List] }
}}
/>
<Button icon={IconAdd} label={recruit.string.CompanyCreateLabel} kind={'accented'} on:click={showCreateDialog} />
</div>
</div>
Expand All @@ -211,16 +219,6 @@
<FilterButton _class={recruit.mixin.VacancyList} />
</div>
<div class="ac-header-full medium-gap">
<ViewletSelector
hidden
viewletQuery={{
attachTo: recruit.mixin.VacancyList,
descriptor: view.viewlet.Table
}}
bind:preference
bind:loading
bind:viewlet
/>
<ViewletSettingButton bind:viewOptions bind:viewlet />
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
</div>
Expand All @@ -234,19 +232,22 @@
on:change={(e) => (resultQuery = e.detail)}
/>

{#if viewlet}
{#if loading}
<Loading />
{:else}
<TableBrowser
_class={recruit.mixin.VacancyList}
config={createConfig(viewlet, preference)}
options={viewlet.options}
query={{
{#if loading}
<Loading />
{:else if viewlet && viewlet?.$lookup?.descriptor?.component}
<Component
is={viewlet.$lookup.descriptor.component}
props={{
_class: recruit.mixin.VacancyList,
options: viewlet.options,
config: finalConfig,
viewlet,
viewOptions,
viewOptionsConfig: viewlet.viewOptions?.other,
query: {
...resultQuery
}}
totalQuery={{}}
showNotification
/>
{/if}
},
totalQuery: {}
}}
/>
{/if}
40 changes: 37 additions & 3 deletions plugins/view-resources/src/components/list/List.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
import { Class, Doc, DocumentQuery, FindOptions, Ref, Space, RateLimitter } from '@hcengineering/core'
import { IntlString, getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
Expand Down Expand Up @@ -43,11 +43,16 @@

export let documents: Doc[] | undefined = undefined

const limiter = new RateLimitter(() => ({ rate: 10 }))

let docs: Doc[] = []
let fastDocs: Doc[] = []
let slowDocs: Doc[] = []

$: orderBy = viewOptions.orderBy

const docsQuery = createQuery()
const docsQuerySlow = createQuery()

$: lookup = buildConfigLookup(client.getHierarchy(), _class, config, options?.lookup)
$: resultOptions = { ...options, lookup, ...(orderBy !== undefined ? { sort: { [orderBy[0]]: orderBy[1] } } : {}) }
Expand All @@ -59,12 +64,14 @@

$: queryNoLookup = noLookup(resultQuery)

let fastQueryIds: Ref<Doc>[] = []
$: if (documents === undefined) {
docsQuery.query(
_class,
queryNoLookup,
(res) => {
docs = res
fastDocs = res
fastQueryIds = res.map((it) => it._id)
},
{
...resultOptions,
Expand All @@ -73,11 +80,37 @@
_id: 1,
_class: 1,
...getProjection(viewOptions.groupBy, queryNoLookup)
}
},
limit: 1000
}
)
} else {
docsQuery.unsubscribe()
}
$: if (fastQueryIds.length > 0) {
docsQuerySlow.query(
_class,
{ ...queryNoLookup, _id: { $nin: fastQueryIds } },
(res) => {
slowDocs = res
},
{
...resultOptions,
projection: {
...resultOptions.projection,
_id: 1,
_class: 1,
...getProjection(viewOptions.groupBy, queryNoLookup)
}
}
)
} else {
docsQuerySlow.unsubscribe()
}

$: if (documents === undefined) {
docs = [...fastDocs, ...slowDocs]
} else {
docs = documents
}

Expand Down Expand Up @@ -170,6 +203,7 @@
{viewOptions}
{viewOptionsConfig}
{selectedObjectIds}
{limiter}
level={0}
groupPersistKey={''}
{createItemDialog}
Expand Down
Loading