Skip to content

Commit

Permalink
Project tasks loading only when needed (cvat-ai#3223)
Browse files Browse the repository at this point in the history
* Tasks loading only when needed

* Fixed project page

* Added CHANGELOG, increased packages versions

* Update CHANGELOG.md

Co-authored-by: Boris Sekachev <boris.sekachev@intel.com>

* Fixed comments

* Fixed overflow issue

* Fixed reducer issue

* Fixed cvat-core tests

Co-authored-by: Boris Sekachev <boris.sekachev@intel.com>
  • Loading branch information
ActiveChooN and Boris Sekachev committed May 25, 2021
1 parent ae175d9 commit a17d054
Show file tree
Hide file tree
Showing 18 changed files with 152 additions and 108 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

-
- Project page requests took a long time and did many DB queries (<https://github.com/openvinotoolkit/cvat/pull/3223>)

### Security

Expand Down
2 changes: 1 addition & 1 deletion cvat-core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cvat-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.12.3",
"version": "3.13.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
Expand Down
11 changes: 11 additions & 0 deletions cvat-core/src/api-implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,14 @@

checkExclusiveFields(filter, ['id', 'search'], ['page', 'withoutTasks']);

if (typeof filter.withoutTasks === 'undefined') {
if (typeof filter.id === 'undefined') {
filter.withoutTasks = true;
} else {
filter.withoutTasks = false;
}
}

const searchParams = new URLSearchParams();
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page', 'withoutTasks']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
Expand All @@ -238,7 +246,10 @@
// prettier-ignore
const projects = projectsData.map((project) => {
if (filter.withoutTasks) {
project.task_ids = project.tasks;
project.tasks = [];
} else {
project.task_ids = project.tasks.map((task) => task.id);
}
return project;
}).map((project) => new Project(project));
Expand Down
30 changes: 28 additions & 2 deletions cvat-core/src/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const { Label } = require('./labels');
const { getPreview } = require('./frames');
const User = require('./user');

/**
Expand All @@ -34,6 +35,7 @@
updated_date: undefined,
task_subsets: undefined,
training_project: undefined,
task_ids: undefined,
};

for (const property in data) {
Expand All @@ -58,9 +60,9 @@
data.tasks.push(taskInstance);
}
}
if (!data.task_subsets && data.tasks.length) {
if (!data.task_subsets) {
const subsetsSet = new Set();
for (const task in data.tasks) {
for (const task of data.tasks) {
if (task.subset) subsetsSet.add(task.subset);
}
data.task_subsets = Array.from(subsetsSet);
Expand Down Expand Up @@ -254,6 +256,22 @@
);
}

/**
* Get the first frame of the first task of a project for preview
* @method preview
* @memberof Project
* @returns {string} - jpeg encoded image
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async preview() {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.preview);
return result;
}

/**
* Method updates data of a created project or creates new project from scratch
* @method save
Expand Down Expand Up @@ -331,4 +349,12 @@
const result = await serverProxy.projects.delete(this.id);
return result;
};

Project.prototype.preview.implementation = async function () {
if (!this._internalData.task_ids.length) {
return '';
}
const frameData = await getPreview(this._internalData.task_ids[0]);
return frameData;
};
})();
2 changes: 1 addition & 1 deletion cvat-core/tests/api/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const { Project } = require('../../src/project');

describe('Feature: get projects', () => {
test('get all projects', async () => {
const result = await window.cvat.projects.get();
const result = await window.cvat.projects.get({ withoutTasks: false });
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(2);
for (const el of result) {
Expand Down
14 changes: 8 additions & 6 deletions cvat-core/tests/mocks/server-proxy.mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ const {
frameMetaDummyData,
} = require('./dummy-data.mock');

function QueryStringToJSON(query) {
function QueryStringToJSON(query, ignoreList = []) {
const pairs = [...new URLSearchParams(query).entries()];

const result = {};
for (const pair of pairs) {
const [key, value] = pair;
if (['id'].includes(key)) {
result[key] = +value;
} else {
result[key] = value;
if (!ignoreList.includes(key)) {
if (['id'].includes(key)) {
result[key] = +value;
} else {
result[key] = value;
}
}
}

Expand Down Expand Up @@ -73,7 +75,7 @@ class ServerProxy {
}

async function getProjects(filter = '') {
const queries = QueryStringToJSON(filter);
const queries = QueryStringToJSON(filter, ['without_tasks']);
const result = projectsDummyData.results.filter((x) => {
for (const key in queries) {
if (Object.prototype.hasOwnProperty.call(queries, key)) {
Expand Down
2 changes: 1 addition & 1 deletion cvat-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cvat-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.20.0",
"version": "1.20.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
Expand Down
72 changes: 36 additions & 36 deletions cvat-ui/src/actions/projects-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
import { Dispatch, ActionCreator } from 'redux';

import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { ProjectsQuery, CombinedState } from 'reducers/interfaces';
import { ProjectsQuery } from 'reducers/interfaces';
import { getTasksSuccess, updateTaskSuccess } from 'actions/tasks-actions';
import { getCVATStore } from 'cvat-store';
import getCore from 'cvat-core-wrapper';

const cvat = getCore();
Expand All @@ -31,8 +30,8 @@ export enum ProjectsActionTypes {
// prettier-ignore
const projectActions = {
getProjects: () => createAction(ProjectsActionTypes.GET_PROJECTS),
getProjectsSuccess: (array: any[], count: number) => (
createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, count })
getProjectsSuccess: (array: any[], previews: string[], count: number) => (
createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, previews, count })
),
getProjectsFailed: (error: any) => createAction(ProjectsActionTypes.GET_PROJECTS_FAILED, { error }),
updateProjectsGettingQuery: (query: Partial<ProjectsQuery>) => (
Expand Down Expand Up @@ -60,7 +59,7 @@ const projectActions = {
export type ProjectActions = ActionUnion<typeof projectActions>;

export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
return async (dispatch: ActionCreator<Dispatch>, getState): Promise<void> => {
dispatch(projectActions.getProjects());
dispatch(projectActions.updateProjectsGettingQuery(query));

Expand All @@ -69,6 +68,7 @@ export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
page: 1,
...query,
};

for (const key in filteredQuery) {
if (filteredQuery[key] === null || typeof filteredQuery[key] === 'undefined') {
delete filteredQuery[key];
Expand All @@ -85,38 +85,38 @@ export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {

const array = Array.from(result);

const tasks: any[] = [];
const taskPreviewPromises: Promise<any>[] = [];

for (const project of array) {
taskPreviewPromises.push(
...(project as any).tasks.map((task: any): string => {
tasks.push(task);
return (task as any).frames.preview().catch(() => '');
}),
);
}
// Appropriate tasks fetching proccess needs with retrieving only a single project
if (Object.keys(filteredQuery).includes('id')) {
const tasks: any[] = [];
const [project] = array;
const taskPreviewPromises: Promise<string>[] = (project as any).tasks.map((task: any): string => {
tasks.push(task);
return (task as any).frames.preview().catch(() => '');
});

const taskPreviews = await Promise.all(taskPreviewPromises);

dispatch(projectActions.getProjectsSuccess(array, result.count));

const store = getCVATStore();
const state: CombinedState = store.getState();

if (!state.tasks.fetching) {
dispatch(
getTasksSuccess(tasks, taskPreviews, tasks.length, {
page: 1,
assignee: null,
id: null,
mode: null,
name: null,
owner: null,
search: null,
status: null,
}),
);
const taskPreviews = await Promise.all(taskPreviewPromises);

const state = getState();

dispatch(projectActions.getProjectsSuccess(array, taskPreviews, result.count));

if (!state.tasks.fetching) {
dispatch(
getTasksSuccess(tasks, taskPreviews, tasks.length, {
page: 1,
assignee: null,
id: null,
mode: null,
name: null,
owner: null,
search: null,
status: null,
}),
);
}
} else {
const previewPromises = array.map((project): string => (project as any).preview().catch(() => ''));
dispatch(projectActions.getProjectsSuccess(array, await Promise.all(previewPromises), result.count));
}
};
}
Expand Down
10 changes: 4 additions & 6 deletions cvat-ui/src/components/project-page/project-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,16 @@ export default function ProjectPageComponent(): JSX.Element {
const id = +useParams<ParamType>().id;
const dispatch = useDispatch();
const history = useHistory();
const projects = useSelector((state: CombinedState) => state.projects.current);
const projects = useSelector((state: CombinedState) => state.projects.current).map((project) => project.instance);
const projectsFetching = useSelector((state: CombinedState) => state.projects.fetching);
const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes);
const taskDeletes = useSelector((state: CombinedState) => state.tasks.activities.deletes);
const tasksActiveInferences = useSelector((state: CombinedState) => state.models.inferences);
const tasks = useSelector((state: CombinedState) => state.tasks.current);
const projectSubsets = useSelector((state: CombinedState) => {
const [project] = state.projects.current.filter((_project) => _project.id === id);
return project ? ([...new Set(project.tasks.map((task: any) => task.subset))] as string[]) : [];
});

const [project] = projects.filter((_project) => _project.id === id);
const projectSubsets = [''];
if (project) projectSubsets.push(...project.subsets);
const deleteActivity = project && id in deletes ? deletes[id] : null;

useEffect(() => {
Expand Down Expand Up @@ -90,7 +88,7 @@ export default function ProjectPageComponent(): JSX.Element {
</Button>
</Col>
</Row>
{projectSubsets.map((subset) => (
{projectSubsets.map((subset: string) => (
<React.Fragment key={subset}>
{subset && <Title level={4}>{subset}</Title>}
{tasks
Expand Down
5 changes: 5 additions & 0 deletions cvat-ui/src/components/project-page/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

@import '../../base.scss';

.cvat-project-page {
overflow-y: auto;
height: 100%;
}

.cvat-project-details {
width: 100%;
height: auto;
Expand Down
30 changes: 12 additions & 18 deletions cvat-ui/src/components/projects-page/project-item.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT

Expand All @@ -22,24 +22,18 @@ interface Props {
}

export default function ProjectItemComponent(props: Props): JSX.Element {
const { projectInstance } = props;
const {
projectInstance: { instance, preview },
} = props;

const history = useHistory();
const ownerName = projectInstance.owner ? projectInstance.owner.username : null;
const updated = moment(projectInstance.updatedDate).fromNow();
const ownerName = instance.owner ? instance.owner.username : null;
const updated = moment(instance.updatedDate).fromNow();
const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes);
const deleted = projectInstance.id in deletes ? deletes[projectInstance.id] : false;

let projectPreview = null;
if (projectInstance.tasks.length) {
// prettier-ignore
projectPreview = useSelector((state: CombinedState) => (
state.tasks.current.find((task) => task.instance.id === projectInstance.tasks[0].id)?.preview
));
}
const deleted = instance.id in deletes ? deletes[instance.id] : false;

const onOpenProject = (): void => {
history.push(`/projects/${projectInstance.id}`);
history.push(`/projects/${instance.id}`);
};

const style: React.CSSProperties = {};
Expand All @@ -52,10 +46,10 @@ export default function ProjectItemComponent(props: Props): JSX.Element {
return (
<Card
cover={
projectPreview ? (
preview ? (
<img
className='cvat-projects-project-item-card-preview'
src={projectPreview}
src={preview}
alt='Preview'
onClick={onOpenProject}
aria-hidden
Expand All @@ -73,7 +67,7 @@ export default function ProjectItemComponent(props: Props): JSX.Element {
<Meta
title={(
<span onClick={onOpenProject} className='cvat-projects-project-item-title' aria-hidden>
{projectInstance.name}
{instance.name}
</span>
)}
description={(
Expand All @@ -88,7 +82,7 @@ export default function ProjectItemComponent(props: Props): JSX.Element {
<Text type='secondary'>{`Last updated ${updated}`}</Text>
</div>
<div>
<Dropdown overlay={<ProjectActionsMenuComponent projectInstance={projectInstance} />}>
<Dropdown overlay={<ProjectActionsMenuComponent projectInstance={instance} />}>
<Button type='link' size='large' icon={<MoreOutlined />} />
</Dropdown>
</div>
Expand Down
Loading

0 comments on commit a17d054

Please sign in to comment.