doug-martin / nestjs-query

Easy CRUD for GraphQL.
https://doug-martin.github.io/nestjs-query
MIT License
820 stars 142 forks source link

Scaling bottleneck #850

Open dasimandl opened 3 years ago

dasimandl commented 3 years ago

Hello,

First off I would like to say that this library is exceptionally powerful, well thought out and well documented. I was having a lot of success during my prototyping phase, but I'm now facing some scaling issues as I implement in a production environment.

Have you read the Contributing Guidelines? YES

To Reproduce I do not have an easy way of reproducing this issue as I am working on private repositories. Before I create a mocked scaled out application example for this, I wanted to see if anyone else has been faced with a similar scaling issues.

Expected behavior This is more of a question around scaling the number of auto-generated resolvers. Below is an explanation of the issue I was faced with.

Explanation of test: I setup perf_hooks to calculate the time it takes for the AppModule to be imported (this is where GraphQL is initiated) within the main.ts file. I compared the cost of additional entities incrementing 5 at a time. Currently scale of the application is 74 entities setup with GraphQL decorators, but will continue to grow overtime.

Count (Resolvers) Load Time (seconds) change (seconds)
1 0.5 -
5 0.75 0.25
10 1 0.25
15 1.8 0.8
20 3.5 1.7
25 5.5 2
30 8.2 2.7
35 10 1.8
40 14 4
45 17.5 3.5
50 21.2 3.7
55 26.5 5.3
60 35.5 9
65 45 9.5
70 54.5 9.5
74 67 12.5

NOTE: These times are not exact, however I tried to run the test at lease two times and take an average.

Desktop:

Additional context N/A

doug-martin commented 3 years ago

@dasimandl this is interesting I wonder if this is nests-query or @nestjs/graphql and the schema generation. If you could provide a sample repo I can look into this some more to see where the bottle neck is. If its in nestjs/graphql we may want to open the issue there.

dasimandl commented 3 years ago

@doug-martin Hello sorry for the long delay on this. I finally got around to creating a demo application and hosted on a public github repo. https://github.com/dasimandl/nestjs-query-scaling-bug-demo

Performance results: (see README for steps to reproduce)

NOTE: This was conducted by incrementally uncommenting the resolvers and imports from NestjsQueryGraphQLModule in the demo.module.ts file. The data below was gathered by saving to trigger a reload three times and doing a mental average/rounding.

Count (Resolvers) Load Time (ms) change (ms)
0 150 -
1 200 50
5 400 200
10 750 350
15 1000 250
20 1200 200
25 1450 250
30 1650 200
35 1850 200
40 2100 250
45 2350 250
50 2600 250
55 2750 150
60 3000 250
65 3300 300
70 3650 350
83 4500 850
dasimandl commented 3 years ago

@doug-martin I am following up to see if you had a chance to review this bug yet?

dasimandl commented 3 years ago

@doug-martin I am following to check the status on this open issue?

meodemsao commented 2 years ago

@dasimandl maybe i have same problem https://github.com/doug-martin/nestjs-query/issues/1452

meodemsao commented 2 years ago

i profiler and find this function need many time

Screen Shot 2021-11-03 at 12 43 27 PM Screen Shot 2021-11-03 at 12 52 06 PM

meodemsao commented 2 years ago

@doug-martin

in crud.resolver.ts file

import { PagingStrategies } from '../types';
import { Aggregateable, AggregateResolverOpts, AggregateResolver } from './aggregate.resolver';
import { Relatable } from './relations';
import { Readable, ReadResolverFromOpts, ReadResolverOpts } from './read.resolver';
import { Creatable, CreateResolver, CreateResolverOpts } from './create.resolver';
import { Referenceable, ReferenceResolverOpts } from './reference.resolver';
import { MergePagingStrategyOpts, ResolverClass } from './resolver.interface';
import { Updateable, UpdateResolver, UpdateResolverOpts } from './update.resolver';
import { DeleteResolver, DeleteResolverOpts } from './delete.resolver';
import { BaseResolverOptions } from '../decorators/resolver-method.decorator';
import { mergeBaseResolverOpts } from '../common';
import { RelatableOpts } from './relations/relations.resolver';
import { CursorConnectionOptions } from '../types/connection/cursor';
import { performance } from 'perf_hooks';

export interface CRUDResolverOpts<
  DTO,
  C = DeepPartial<DTO>,
  U = DeepPartial<DTO>,
  R extends ReadResolverOpts<DTO> = ReadResolverOpts<DTO>,
  PS extends PagingStrategies = PagingStrategies.CURSOR
> extends BaseResolverOptions,
    Pick<CursorConnectionOptions, 'enableTotalCount'> {
  /**
   * The DTO that should be used as input for create endpoints.
   */
  CreateDTOClass?: Class<C>;
  /**
   * The DTO that should be used as input for update endpoints.
   */
  UpdateDTOClass?: Class<U>;
  enableSubscriptions?: boolean;
  pagingStrategy?: PS;
  enableAggregate?: boolean;
  create?: CreateResolverOpts<DTO, C>;
  read?: R;
  update?: UpdateResolverOpts<DTO, U>;
  delete?: DeleteResolverOpts<DTO>;
  referenceBy?: ReferenceResolverOpts;
  aggregate?: AggregateResolverOpts;
}

export interface CRUDResolver<
  DTO,
  C,
  U,
  R extends ReadResolverOpts<DTO>,
  QS extends QueryService<DTO, C, U> = QueryService<DTO, C, U>
> extends CreateResolver<DTO, C, QS>,
    ReadResolverFromOpts<DTO, R, QS>,
    UpdateResolver<DTO, U, QS>,
    DeleteResolver<DTO, QS>,
    AggregateResolver<DTO, QS> {}

/**
 * Factory to create a resolver that includes all CRUD methods from [[CreateResolver]], [[ReadResolver]],
 * [[UpdateResolver]], and [[DeleteResolver]].
 *
 * ```ts
 * import { CRUDResolver } from '@nestjs-query/query-graphql';
 * import { Resolver } from '@nestjs/graphql';
 * import { TodoItemDTO } from './dto/todo-item.dto';
 * import { TodoItemService } from './todo-item.service';
 *
 * @Resolver()
 * export class TodoItemResolver extends CRUDResolver(TodoItemDTO) {
 *   constructor(readonly service: TodoItemService) {
 *     super(service);
 *   }
 * }
 * ```
 * @param DTOClass - The DTO Class that the resolver is for. All methods will use types derived from this class.
 * @param opts - Options to customize the resolver.
 */
// eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional
export const CRUDResolver = <
  DTO,
  C = DeepPartial<DTO>,
  U = DeepPartial<DTO>,
  R extends ReadResolverOpts<DTO> = ReadResolverOpts<DTO>,
  PS extends PagingStrategies = PagingStrategies.CURSOR
>(
  DTOClass: Class<DTO>,
  opts: CRUDResolverOpts<DTO, C, U, R, PS> = {},
): ResolverClass<DTO, QueryService<DTO, C, U>, CRUDResolver<DTO, C, U, MergePagingStrategyOpts<DTO, R, PS>>> => {
  const {
    CreateDTOClass,
    UpdateDTOClass,
    enableSubscriptions,
    pagingStrategy,
    enableTotalCount,
    enableAggregate,
    create = {},
    read = {},
    update = {},
    delete: deleteArgs = {},
    referenceBy = {},
    aggregate,
  } = opts;

  const referencableTime = performance.now()
  const referencable = Referenceable(DTOClass, referenceBy);
  const startTimeRelatable = performance.now()
  const relatable = Relatable(
    DTOClass,
    mergeBaseResolverOpts({ enableTotalCount, enableAggregate } as RelatableOpts, opts),
  );
  const endTimeRelatable = performance.now()
  console.log(`Readable............................: ${endTimeRelatable - startTimeRelatable}ms`);

  const startTimeAggregateable = performance.now()
  const aggregateable = Aggregateable(DTOClass, {
    enabled: enableAggregate,
    ...mergeBaseResolverOpts(aggregate ?? {}, opts),
  });
  const endTimeAggregateable = performance.now()
  console.log(`Aggregateable............................: ${endTimeAggregateable - startTimeAggregateable}ms`);

  const startTimeCreatable = performance.now()
  const creatable = Creatable(DTOClass, {
    CreateDTOClass,
    enableSubscriptions,
    ...mergeBaseResolverOpts(create ?? {}, opts),
  });
  const endTimeCreatable = performance.now()
  console.log(`Creatable............................: ${endTimeCreatable - startTimeCreatable}ms`);

  const startTimeReadable = performance.now()
  const readable = Readable(DTOClass, {
    enableTotalCount,
    pagingStrategy,
    ...mergeBaseResolverOpts(read, opts),
  } as MergePagingStrategyOpts<DTO, R, PS>);
  const endTimeReadable = performance.now()
  console.log(`Readable............................: ${endTimeReadable - startTimeReadable}ms`);

  const startTimeUpdateable = performance.now()
  const updateable = Updateable(DTOClass, {
    UpdateDTOClass,
    enableSubscriptions,
    ...mergeBaseResolverOpts(update, opts),
  });
  const endTimeUpdateable = performance.now()
  console.log(`Updateable............................: ${endTimeUpdateable - startTimeUpdateable}ms`);
  const startTimeDelete = performance.now()
  const deleteResolver = DeleteResolver(DTOClass, { enableSubscriptions, ...mergeBaseResolverOpts(deleteArgs, opts) });
  const endTimeDelete = performance.now()
  console.log(`Delete............................: ${endTimeDelete - startTimeDelete}ms`);

  // return referencable(relatable(aggregateable(creatable(readable(updateable(deleteResolver))))));
  return referencable(relatable(aggregateable(creatable(readable(updateable(deleteResolver))))));
};

resum

Screen Shot 2021-11-03 at 2 10 55 PM

i don't know why deleteResolver need many time to init

meodemsao commented 2 years ago

@doug-martin in query-graphql/common/external.utils.ts i put some console log

export function getGraphqlEnumMetadata(objType: object): EnumMetadata | undefined {
  // hack to get enums loaded it may break in the future :(
    const generateStartTime = performance.now()
    console.log(chalk.cyan('TypeMetadataStorage.getEnumsMetadata()....................', TypeMetadataStorage.getEnumsMetadata()))
  LazyMetadataStorage.load();
  const result =  TypeMetadataStorage.getEnumsMetadata().find((o) => o.ref === objType);
    const generateEndTime = performance.now()
    console.log(chalk.cyan(`getGraphqlEnumMetadata...........................`, result));
    console.log(chalk.cyan(`getGraphqlEnumMetadata........................... ${generateEndTime - generateStartTime}ms`));
  return result
}

And have need many time to execute function TypeMetadataStorage.getEnumsMetadata().find((o) => o.ref === objType);

Because need to find in big array bellow return from TypeMetadataStorage.getEnumsMetadata()

Screen Shot 2021-11-04 at 3 49 28 PM

pratiksyngenta commented 2 years ago

I hope this gets resolved in upcoming version(s), I really want to use this powerful solution <3

pratiksyngenta commented 2 years ago

Hey @dasimandl @meodemsao ; do you recommend any workaround for this problem?

meodemsao commented 2 years ago

@pratiksyngenta i hot fixed this problem by add more enumName properties in decorate and not use function

  // hack to get enums loaded it may break in the future :(
    const generateStartTime = performance.now()
    console.log(chalk.cyan('TypeMetadataStorage.getEnumsMetadata()....................', TypeMetadataStorage.getEnumsMetadata()))
  LazyMetadataStorage.load();
  const result =  TypeMetadataStorage.getEnumsMetadata().find((o) => o.ref === objType);
    const generateEndTime = performance.now()
    console.log(chalk.cyan(`getGraphqlEnumMetadata...........................`, result));
    console.log(chalk.cyan(`getGraphqlEnumMetadata........................... ${generateEndTime - generateStartTime}ms`));
  return result
}