Skip to content

Commit

Permalink
Merge pull request lvgl#55 from bubeck/feature/optimize
Browse files Browse the repository at this point in the history
use --optimize to generate faster code without strcmp at runtime
  • Loading branch information
kisvegabor authored Oct 11, 2024
2 parents 807a022 + d6ea22e commit c36ae9f
Show file tree
Hide file tree
Showing 10 changed files with 351 additions and 159 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,17 @@ lv_i18n compile -t 'translations/*.yml' -o 'src/lv_i18n'

The default locale is `en-GB` but you change it with `-l 'language-code'`.

You can use `--optimize` to generate optimized C code. Without this, finding the corresponding translation by `lv_i18n_get_text()` is done by searching through all keys until the right one is found. This can eat up a lot of CPU espcially if the list is long (aka O(n)). Using `--optimize` changes this behaviour by using an integer index into the list of strings resulting in an immediate return of the right string (aka O(1)). As the index is computed at compile time, you need a compiler, which is able to evaluate `strcmp()` with constants at compile time. All modern compilers, like gcc and clang are able to do this. If you want to check, whether your compiler is able to handle this optimization, you can use the following code to check this:

```c
int main()
{
return strcmp("a", "a");
}
```

If this compiles without needing `#include <string.h>` and `nm -u a.out` does not output `strcmp` as being undefined, then the compiler optimizes the code and is able to handle `--optimize`.

## Follow modifications in the source code
To change a text id in the `yml` files use:
```sh
Expand Down
43 changes: 38 additions & 5 deletions lib/cmd_compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ const TranslationKeys = require('./translation_keys');
const AppError = require('./app_error');
const shell = require('shelljs');

const { join } = require('path');
const compiler_template = require('./compiler_template');
const { join, dirname, basename, extname } = require('path');
const { getRAW, getIDX } = require('./compiler_template');

const { readFileSync, writeFileSync } = require('fs');

Expand Down Expand Up @@ -47,6 +47,15 @@ module.exports.subparserArgsList = [
metavar: '<path>'
}
},
{
args: [ '--optimize' ],
options: {
dest: 'optimize',
help: 'Use integers as keys instead of strings to speed up lookup',
action: 'store_true',
default: false
}
},
{
args: [ '-l' ],
options: {
Expand Down Expand Up @@ -112,13 +121,27 @@ You did not specified locale and we could not autodetect it. Use '-l' option.
//
// Fill data
//
let data = {};
let data = {
singularKeys: [],
pluralKeys: []
};

// Pre-fill .singular & plural props for edge case - missed locale
sorted_locales.forEach(l => {
data[l] = { singular: {}, plural: {} };
});

translationKeys.phrases.forEach(p => {
if (p.value?.constructor !== Object) {
if (!data.singularKeys.includes(p.key)) {
data.singularKeys.push(p.key);
}
} else if (!data.pluralKeys.includes(p.key)) {
data.pluralKeys.push(p.key);
}
});
data.singularKeys.sort();
data.pluralKeys.sort();

translationKeys.phrases.forEach(p => {
if (p.value === null) return;
Expand All @@ -140,18 +163,28 @@ You did not specified locale and we could not autodetect it. Use '-l' option.

});

let raw = compiler_template(sorted_locales, data);
let raw_idx;
if (args.optimize) raw_idx = '#define LV_I18N_OPTIMIZE 1\n';
else raw_idx = '#undef LV_I18N_OPTIMIZE\n';
raw_idx += getIDX(data);
let raw = getRAW(args, sorted_locales, data);

if (args.output_raw) {
writeFileSync(args.output_raw, raw);
let output_raw_header = join(dirname(args.output_raw), basename(args.output_raw, extname(args.output_raw)) + '.h');
writeFileSync(output_raw_header, raw_idx);
}

if (args.output) {
if (!shell.test('-d', args.output)) {
throw new AppError(`Output directory not exists (${args.output})`);
}

shell.cp(join(__dirname, '../src/lv_i18n.h'), join(args.output, 'lv_i18n.h'));
let txt_h = readFileSync(join(__dirname, '../src/lv_i18n.template.h'), 'utf-8');
txt_h = txt_h.replace(/\/\*SAMPLE_START\*\/([\s\S]+)\/\*SAMPLE_END\*\//,
`${raw_idx}
////////////////////////////////////////////////////////////////////////////////`);
writeFileSync(join(args.output, 'lv_i18n.h'), txt_h);

let txt = readFileSync(join(__dirname, '../src/lv_i18n.template.c'), 'utf-8');

Expand Down
94 changes: 79 additions & 15 deletions lib/compiler_template.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,53 @@ static inline uint32_t op_e(uint32_t val) { UNUSED(val); return 0; }

function lang_plural_template(l, form, data) {
const loc = to_c(l);
return `
static const lv_i18n_phrase_t ${loc}_plurals_${form}[] = {
${Object.entries(data[l].plural[form]).map(([ k, val ]) => ` {"${esc(k)}", "${esc(val)}"},`).join('\n')}
{NULL, NULL} // End mark
};
`.trim();
let result = '';

result = `
static const char * ${loc}_plurals_${form}[] = {
`;

let index = 0;
Object.values(data.pluralKeys).forEach(k => {
if (!data[l].plural || !data[l].plural[form] || !data[l].plural[form][k]) {
result += ' NULL, // ' + index + '=\"' + esc(k) + '\"\n';
} else {
result += ' \"' + esc(data[l].plural[form][k]) + '\", // ' + index + '=\"' + esc(k) + '\"\n';
}
index++;
});

result += `};`;

return result.trim();

}

function lang_singular_template(l, data) {
const loc = to_c(l);
return `
static const lv_i18n_phrase_t ${loc}_singulars[] = {
${Object.entries(data[l].singular).map(([ k, val ]) => ` {"${esc(k)}", "${esc(val)}"},`).join('\n')}
{NULL, NULL} // End mark
};
`.trim();
let result = '';

result = `
static const char * ${loc}_singulars[] = {
`;

let index = 0;
Object.values(data.singularKeys).forEach(k => {
if (!data[l].singular || !data[l].singular[k]) {
result += ' NULL, // ' + index + '=\"' + esc(k) + '\"\n';
} else {
result += ' \"' + esc(data[l].singular[k]) + '\", // ' + index + '=\"' + esc(k) + '\"\n';
}
index++;
});

result += `};`;

return result.trim();
}


function lang_template(l, data) {
function lang_template(args, l, data) {
let pforms = Object.keys(data[l].plural);
const loc = to_c(l);

Expand All @@ -86,16 +113,53 @@ ${pforms.map(pf => ` .plurals[${pf_enum[pf]}] = ${loc}_plurals_${pf},`).join(
`.trim();
}

function generate_idx(keys) {
let result = '';
Object.values(keys).forEach(k => {
result += '\"' + esc(k) + '\",\n';
});

return result;
}

function getIDX2(keys, i) {
let result = 'LV_I18N_ID_NOT_FOUND';

let key = keys[i];
if (key) {
result = '(!strcmp(str, \"' + esc(key) + '\")?' + i + ':' + getIDX2(keys, i + 1) + ')';
}
return result;
}

module.exports.getIDX = function (data) {
let result = '#define LV_I18N_IDX_s(str) ' + getIDX2(data.singularKeys, 0) + '\n';
result += '#define LV_I18N_IDX_p(str) ' + getIDX2(data.pluralKeys, 0) + '\n';
return result;
};

module.exports = function (locales, data) {
module.exports.getRAW = function (args, locales, data) {
return `
${plural_helpers}
${locales.map(l => lang_template(l, data)).join('\n\n')}
${locales.map(l => lang_template(args, l, data)).join('\n\n')}
const lv_i18n_language_pack_t lv_i18n_language_pack[] = {
${locales.map(l => ` &${to_c(l)}_lang,`).join('\n')}
NULL // End mark
};
#ifndef LV_I18N_OPTIMIZE
static const char * singular_idx[] = {
${generate_idx(data.singularKeys)}
};
static const char * plural_idx[] = {
${generate_idx(data.pluralKeys)}
};
#endif
`;
};
Loading

0 comments on commit c36ae9f

Please sign in to comment.