We use cookies to enhance your experience on the site
CodeWorlds

PROJECT - a performance optimisation system

This module showed several tools: a cache in the process's memory and in Redis, database indexes, response compression, limiting the number of requests. The project is where you must decide which one to use - and that is a quite different skill from knowing the syntax.

You will build a legion management API that copes with more traffic than a naive version would bear, and can show why it copes.

One rule before all the others

Optimisation without measurement is guesswork. It sounds obvious, and it is the most frequently broken rule in the field: a cache is added wherever it happened to come to mind, and an improvement nobody measured is duly recorded.

The order of work is always the same:

  1. Measure - how long a request takes, how much memory it uses, how many queries go to the database.
  2. Find the bottleneck - one, the most costly.
  3. Apply the cheapest fix that removes it.
  4. Measure again - and check that the fix worked rather than merely moving the problem.

That is why the project begins with measurement, not with a cache.

Step 1 - the metrics endpoint

The first thing you expose is a view of the process's state:

1@Controller('metrics')
2export class MetricsController {
3  @Get()
4  getMetrics() {
5    const memory = process.memoryUsage();
6
7    return {
8      heapUsed: memory.heapUsed,
9      heapTotal: memory.heapTotal,
10      rss: memory.rss,
11      uptime: process.uptime(),
12    };
13  }
14}

process.memoryUsage()
returns several numbers, and it is worth knowing how they differ.
heapUsed
is the memory actually occupied by your application's objects - it grows when you keep too much in a cache.
heapTotal
is the memory the V8 engine has reserved for the heap; it is always larger and changes in jumps.
rss
(Resident Set Size) is the whole process's memory as the operating system sees it - heap, stack, code and buffers together.

The difference between

heapUsed
and
rss
matters most. When
heapUsed
alone grows, you are accumulating objects - usually in some cache without a limit. When
rss
grows while
heapUsed
stands still, the leak is outside the heap: buffers, open connections, native libraries.

process.uptime()
gives the number of seconds the process has been running. It says little by itself, but a great deal alongside the rest: memory use rising linearly with running time is a leak, not load.

Step 2 - a cache in Redis

The in-process cache you met earlier has one flaw: every instance of the application has its own. With two instances the same read reaches the database twice, and an invalidation in one never reaches the other. Redis fixes that:

1CacheModule.register({
2  store: redisStore,
3  host: 'localhost',
4  port: 6379,
5  ttl: 300 })

The written order is fixed:

CacheModule.register({
opens the configuration,
store: redisStore,
swaps the store from process memory to Redis,
host: 'localhost',
and
port: 6379,
say where that Redis stands, and
ttl: 300 })
sets the default lifetime of an entry and closes.

store
is the crucial line. Without it
CacheModule
still works, but in process memory - and that is treacherous, because locally everything looks fine and the problem appears only once a second instance starts in production.

Note that

host
and
port
sit directly in the configuration object here. Newer versions of
cache-manager
nest them in a separate connection field - check the version in your
package.json
before copying somebody else's example.

Step 3 - the database

A cache speeds up repeated reads. Queries that cannot be cached must be fixed at the source:

1@Entity('legionaries')
2@Index(['cohortId', 'isActive'])
3export class Legionary {
4  @Column()
5  cohortId: number;
6
7  @Column({ default: true })
8  isActive: boolean;
9}
10
11// in the repository
12findActiveByCohort(cohortId: number) {
13  return this.repo.find({
14    where: { cohortId, isActive: true },
15    select: ['id', 'name', 'rank'],
16    take: 50,
17  });
18}

Three fixes at once, each acting differently. A composite index on the columns you genuinely filter by turns a scan of the whole table into a jump to particular rows.

select
fetches only the needed columns - without it you drag fields nobody will use out of the database, bulk and all.
take
limits the number of rows; an endpoint without a limit works splendidly until the day somebody has a hundred thousand of them.

Before adding an index, check the query plan with

EXPLAIN
. An index on a column nobody filters by speeds up no read and slows down every write.

Step 4 - the outward layer

Two things at the application's edge remain:

1async function bootstrap() {
2  const app = await NestFactory.create(AppModule);
3
4  app.use(compression());
5
6  await app.listen(3000);
7}

compression()
packs responses with gzip - for JSON lists that usually means several times less traffic, at the cost of a little processor time. Rate limiting with
ThrottlerModule
works from the other side: it speeds nothing up, it limits the number of requests you accept at all.

These two are worth understanding as a pair of opposites. Compression and caching make one request cost less. Rate limiting makes the requests fewer. When a system is choking you always have those two roads open - and usually you need both.

What you hand in

The project is finished when it contains:

  1. A
    /metrics
    endpoint
    with
    heapUsed
    ,
    heapTotal
    ,
    rss
    and
    uptime
    .
  2. A cache in Redis through
    CacheModule.register
    with
    store: redisStore
    , invalidated on write.
  3. Indexes on the filtered columns, plus
    select
    and
    take
    in listing queries.
  4. Response compression and rate limiting on the write endpoints.
  5. A before-and-after measurement - figures for at least one endpoint, with a note on what changed.

The fifth point matters most and is most often skipped. Without it you are handing in a collection of techniques rather than an optimisation - because an optimisation is the difference between two measurements, @name, not a list of tools applied.

Send the link to your repository when you are done.

Go to CodeWorlds