#optimization 3 hashtags

When you optimize a Crystal program, pay attention to language features that inline code. For example, pay attention to how you use blocks (consequences here and here).

Also pay attention to how you use macros. Macros, like ECR.embed and Slang.embed, inline code at the point where they are invoked. This can be powerful, because macros actually generate code—but, ten invocations later, you have ten copies of the code.

Here's a case of too many copies, but with a very happy ending...

Ktistec uses both ECR.embed and Slang.embed to generate web pages from views and partials. I wrote code to count the number of places Ktistec used embed for each view and partial it renders. There's a long tail, but here are the big ones:

| src/views/layouts/default.html.ecr           | 204 |
| src/views/partials/modals.html.slang         | 204 |
| src/views/partials/header.html.slang         | 204 |
| src/views/partials/footer.html.slang         | 204 |
| src/views/pages/generic.html.slang           | 155 |
| src/views/partials/object/label.html.slang   |  36 |
| src/views/partials/object/content.html.slang |  36 |
| src/views/partials/collection.json.ecr       |  28 |
| src/views/partials/thread.html.slang         |  12 |
| src/views/partials/detail.html.slang         |  12 |
| src/views/partials/object.html.slang         |  12 |
| src/views/partials/actor-panel.html.slang    |  11 |
| src/views/partials/object.json.ecr           |  11 |
| src/views/partials/paginator.html.slang      |  11 |
| src/views/objects/thread.json.ecr            |   8 |
| src/views/partials/activity/label.html.slang |   6 |
| src/views/mentions/index.json.ecr            |   6 |
| src/views/remote_follows/index.json.ecr      |   6 |
| src/views/settings/settings.json.ecr         |   6 |
| src/views/tags/index.json.ecr                |   6 |
| src/views/activities/activity.json.ecr       |   5 |
| src/views/partials/editor.html.slang         |   5 |
| src/views/objects/object.json.ecr            |   5 |
| src/views/actors/remote.json.ecr             |   4 |
...

The layout is part of every page and is rendered with every view, so lots of copies. Every page has a header and a footer (and some default modal dialogs) so you get those, too. The generic view is a little less obvious. It's used to render pages for which there is no more specific view—typically pages served for 400 Bad Request or 401 Unauthorized. Objects (posts) are rendered in a variety of contexts, so it's no surprise label.html.slang and content.html.slang are popular.

ECR.embed and Slang.embed inline templates at the point where they are invoked, but beyond that they don't really customize the generated code—they just duplicate it. What we want  is one function for each view or partial, which wraps embed and returns JSON or HTML.

Those changes mostly occur in commits from 399287cf to 4b025f50. To say that they made a huge difference is a gross understatement. Executable size decreased by ~13%. Build time decreased by ~50%, and the memory required to build decreased by ~30%.

#ktistec #crystallang #optimization

my current canary for build resource utilization is a low end cloud server.  when builds start to fail it's time to optimize.

more on the last round of build optimizations for ktistec, shortly.

#ktistec #optimization

I replaced five indexes* on the relationships table with two**, improved query performance in at least one case, and cut the size of the database down by 11.4% (98MB).

Lessons (finally) learned:

  1. You can have too many indexes. At best, this makes the database larger. In at least one case, however, this caused the query planner to pick a less effective index, which resulted in worse performance.
  2. Unless you understand the data well, it is hard to know what indexes you are going to need up front. For example, on the relationships table, it's clear in retrospect that an index on the to_iri column has better selectivity than an index on the from_iri column—and, in fact, no index is even necessary on the from_iri column. For reasons of symmetry, I created both when I created the table. I'll go so far as to say, don't even create indexes until/unless you can analyze actual data. (Aside: the SQLite3 function likelihood is an excellent way to hint about that data to the query planner.)
  3. Ordering results using the automatically assigned, monotonically increasing id primary key behaves identically to ordering by something like created_at, so order by id and save yourself an index on created_at.

#ktistec #sqlite #optimization

* The original five:

CREATE INDEX idx_relationships_type_from_iri_created_at
    ON relationships (type ASC, from_iri ASC, created_at DESC);
CREATE INDEX idx_relationships_from_iri_created_at_type
    ON relationships (from_iri ASC, created_at DESC, type ASC);
CREATE INDEX idx_relationships_type_to_iri
    ON relationships (type ASC, to_iri ASC);
CREATE INDEX idx_relationships_to_iri_type
    ON relationships (to_iri ASC, type ASC);
CREATE INDEX idx_relationships_type_id
    ON relationships (type ASC, id ASC);


* The final two:

CREATE INDEX idx_relationships_type
    ON relationships (type ASC);
CREATE INDEX idx_relationships_to_iri
    ON relationships (to_iri ASC);