Skip to content

Commit ce7d57d

Browse files
committed
feat: benchmark
1 parent b860513 commit ce7d57d

11 files changed

Lines changed: 613 additions & 17 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ dist-ssr
2222
*.njsproj
2323
*.sln
2424
*.sw?
25+
26+
# Benchmark results
27+
benchmarks/benchmark-results.json

README.md

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44

55
**О Lezer**: Lezer — это парсер-генератор от команды CodeMirror, предназначенный для создания эффективных парсеров с поддержкой инкрементального парсинга. Это делает его идеальным выбором для интеграции в веб-редакторы кода, где требуется быстрое обновление синтаксического дерева при каждом изменении текста. Парсер генерируется из декларативной грамматики и предоставляет структурированное дерево разбора (AST) для дальнейшей обработки кода.
66

7+
## Содержание
8+
9+
- [Особенности](#особенности)
10+
- [Установка](#установка)
11+
- [Использование](#использование)
12+
- [Поддерживаемые конструкции](#поддерживаемые-конструкции)
13+
- [Производительность](#производительность)
14+
- [Разработка](#разработка)
15+
- [Структура проекта](#структура-проекта)
16+
717
## Особенности
818

919
- ✅ Парсинг BSL кода с поддержкой основных конструкций языка
@@ -106,6 +116,48 @@ npm run dev
106116
- Директивы препроцессора (`#`)
107117
- Регистронезависимые ключевые слова
108118

119+
## Производительность
120+
121+
Парсер демонстрирует высокую производительность как при полном парсинге, так и при инкрементальных обновлениях. Ниже представлены результаты бенчмарков для модулей различного размера:
122+
123+
### Модуль: small (93 символов)
124+
125+
| Тест | Операций/сек | Среднее время (ms) | p75 (ms) | p99 (ms) |
126+
|------|-------------|-------------------|----------|----------|
127+
| Полный парсинг | 32,679.15 | 0.0306 | 0.0297 | 0.0595 |
128+
| Инкрементальное обновление - вставка | 8,905.86 | 0.1123 | 0.1306 | 0.7448 |
129+
| Инкрементальное обновление - удаление | 38,448.97 | 0.0260 | 0.0248 | 0.0531 |
130+
131+
### Модуль: medium (652 символов)
132+
133+
| Тест | Операций/сек | Среднее время (ms) | p75 (ms) | p99 (ms) |
134+
|------|-------------|-------------------|----------|----------|
135+
| Полный парсинг | 7,014.13 | 0.1426 | 0.1437 | 0.2633 |
136+
| Инкрементальное обновление - вставка | 4,924.59 | 0.2031 | 0.2153 | 0.7484 |
137+
| Инкрементальное обновление - удаление | 8,326.13 | 0.1201 | 0.1264 | 0.2177 |
138+
139+
### Модуль: large (3420 символов)
140+
141+
| Тест | Операций/сек | Среднее время (ms) | p75 (ms) | p99 (ms) |
142+
|------|-------------|-------------------|----------|----------|
143+
| Полный парсинг | 1,301.79 | 0.7682 | 0.7926 | 1.2203 |
144+
| Инкрементальное обновление - вставка | 1,173.44 | 0.8522 | 0.8845 | 2.0003 |
145+
| Инкрементальное обновление - удаление | 1,341.47 | 0.7455 | 0.7731 | 1.2336 |
146+
147+
### Модуль: very-large (9433 символов)
148+
149+
| Тест | Операций/сек | Среднее время (ms) | p75 (ms) | p99 (ms) |
150+
|------|-------------|-------------------|----------|----------|
151+
| Полный парсинг | 518.98 | 1.9269 | 2.0324 | 3.2636 |
152+
| Инкрементальное обновление - вставка | 1,325.66 | 0.7543 | 0.8249 | 1.6290 |
153+
| Инкрементальное обновление - удаление | 1,250.02 | 0.8000 | 0.8077 | 1.4107 |
154+
155+
Для запуска бенчмарков используйте:
156+
157+
```bash
158+
npm run test:benchmark
159+
```
160+
109161
## Разработка
110162

111163
### Генерация парсера
@@ -150,18 +202,3 @@ lezer-bsl/
150202
├── index.html # HTML для демо-приложения
151203
└── package.json
152204
```
153-
154-
## Технологии
155-
156-
- [Lezer](https://lezer.codemirror.net/) - парсер-генератор
157-
- [TypeScript](https://www.typescriptlang.org/) - язык программирования
158-
- [Vite](https://vitejs.dev/) - сборщик и dev-сервер
159-
- [Vitest](https://vitest.dev/) - тестовый фреймворк
160-
161-
## Лицензия
162-
163-
Apache-2.0
164-
165-
## Репозиторий
166-
167-
https://github.com/alkoleft/lezer-bsl

benchmarks/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Бенчмарки производительности парсера
2+
3+
Этот каталог содержит бенчмарк-тесты для измерения производительности парсера BSL.
4+
5+
## Структура
6+
7+
- `modules/` - тестовые BSL модули разных размеров для тестирования
8+
- `benchmark.bench.ts` - файл с бенчмарками для запуска через `vitest bench` (использует встроенную функциональность vitest)
9+
10+
## Тестовые модули
11+
12+
В каталоге `modules/` находятся следующие модули:
13+
14+
- `small.bsl` - небольшой модуль с одной простой процедурой
15+
- `medium.bsl` - средний модуль с несколькими процедурами и функциями
16+
- `large.bsl` - большой модуль со сложной логикой и множеством операторов
17+
- `very-large.bsl` - очень большой модуль с полной структурой обработки данных
18+
19+
## Запуск бенчмарков
20+
21+
Для запуска бенчмарк-тестов используйте команду:
22+
23+
```bash
24+
npm run test:benchmark
25+
```
26+
27+
Или напрямую через vitest:
28+
29+
```bash
30+
npx vitest bench
31+
```
32+
33+
**Важно:** `vitest bench` использует встроенную функциональность vitest для бенчмарков и автоматически ищет файлы с расширениями `.bench.ts` или `.benchmark.ts`. Результаты выводятся в консоль с подробной статистикой (hz, min, max, mean, percentiles и т.д.).
34+
35+
## Что измеряется
36+
37+
Бенчмарки выполняют следующие замеры для каждого модуля:
38+
39+
1. **Полный парсинг** - время полного парсинга модуля с нуля
40+
2. **Инкрементальная вставка** - время обновления дерева при вставке кода в середину модуля
41+
3. **Инкрементальное удаление** - время обновления дерева при удалении кода из середины модуля
42+
43+
Каждый замер выполняется 50 раз (после 10 итераций разогрева), и выводятся следующие метрики:
44+
- Среднее время (mean)
45+
- Минимальное время (min)
46+
- Максимальное время (max)
47+
48+
## Результаты
49+
50+
После выполнения бенчмарков результаты выводятся в консоль с подробной статистикой для каждого модуля, включая:
51+
- Операций в секунду (hz)
52+
- Минимальное, максимальное и среднее время выполнения
53+
- Процентили (p75, p99, p995, p999)
54+
- Относительную погрешность (rme)
55+
- Сравнение производительности между различными операциями
56+
57+
Результаты можно скопировать из консоли и добавить в описание PR для отслеживания производительности парсера.

benchmarks/benchmark.bench.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, bench } from "vitest"
2+
import { LezerBslParser } from "../src/parser/LezerBslParser"
3+
import * as fs from "node:fs"
4+
import * as path from "node:path"
5+
import { fileURLToPath } from "node:url"
6+
7+
const benchmarksDir = path.dirname(fileURLToPath(import.meta.url))
8+
const modulesDir = path.join(benchmarksDir, "modules")
9+
10+
/**
11+
* Находит ближайшую строку в середине кода
12+
*/
13+
function findMiddleLine(code: string): number {
14+
const lines = code.split('\n')
15+
const middleLineIndex = Math.floor(lines.length / 2)
16+
let position = 0
17+
for (let i = 0; i < middleLineIndex; i++) {
18+
position += lines[i].length + 1 // +1 для символа новой строки
19+
}
20+
return position
21+
}
22+
23+
describe("Бенчмарки производительности парсера", () => {
24+
// Читаем все модули из каталога
25+
const moduleFiles = fs.readdirSync(modulesDir).filter(f => f.endsWith('.bsl'))
26+
27+
if (moduleFiles.length === 0) {
28+
return
29+
}
30+
31+
for (const moduleFile of moduleFiles) {
32+
const modulePath = path.join(modulesDir, moduleFile)
33+
const moduleName = path.basename(moduleFile, '.bsl')
34+
const code = fs.readFileSync(modulePath, 'utf8')
35+
const codeSize = code.length
36+
37+
describe(`Модуль: ${moduleName} (${codeSize} символов)`, () => {
38+
// Полный парсинг
39+
bench(
40+
`Полный парсинг: ${moduleName}`,
41+
() => {
42+
const parser = new LezerBslParser()
43+
parser.parse(code)
44+
},
45+
{ time: 500, iterations: 10 }
46+
)
47+
48+
// Инкрементальное обновление - вставка
49+
{
50+
let parserForInsert: LezerBslParser
51+
52+
bench(
53+
`Инкрементальное обновление - вставка: ${moduleName}`,
54+
() => {
55+
56+
// Измеряется только операция update
57+
const insertPosition = findMiddleLine(code)
58+
const codeToInsert = "\n // Вставленный комментарий для теста\n Перем Тест = 1;"
59+
const codeAfterInsert = code.slice(0, insertPosition) + codeToInsert + code.slice(insertPosition)
60+
61+
parserForInsert.update(codeAfterInsert, [{
62+
rangeOffset: insertPosition,
63+
rangeLength: 0,
64+
text: codeToInsert
65+
}])
66+
},
67+
{
68+
time: 500,
69+
iterations: 10,
70+
setup: () => {
71+
parserForInsert = new LezerBslParser()
72+
parserForInsert.parse(code)
73+
}
74+
}
75+
)
76+
}
77+
78+
// Инкрементальное обновление - удаление
79+
{
80+
let parserForDelete: LezerBslParser
81+
const insertPosition = findMiddleLine(code)
82+
const codeToInsert = "\n // Код для удаления\n Перем Временная = 999;"
83+
const codeWithExtra = code.slice(0, insertPosition) + codeToInsert + code.slice(insertPosition)
84+
85+
bench(
86+
`Инкрементальное обновление - удаление: ${moduleName}`,
87+
() => {
88+
89+
// Измеряется только операция update
90+
parserForDelete.update(code, [{
91+
rangeOffset: insertPosition,
92+
rangeLength: codeToInsert.length,
93+
text: ""
94+
}])
95+
},
96+
{
97+
time: 500,
98+
iterations: 10,
99+
setup: () => {
100+
parserForDelete = new LezerBslParser()
101+
parserForDelete.parse(codeWithExtra)
102+
}
103+
}
104+
)
105+
}
106+
})
107+
}
108+
})

benchmarks/modules/large.bsl

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
Процедура СложнаяОбработка(ВходныеДанные)
2+
Перем Результат = Новый Структура;
3+
Перем ВременныйМассив = Новый Массив;
4+
Перем Индекс = 0;
5+
6+
Попытка
7+
Для Каждого Элемент Из ВходныеДанные Цикл
8+
Если Элемент <> Неопределено Тогда
9+
Перем ОбработанныйЭлемент = ОбработатьЭлемент(Элемент);
10+
11+
Если ОбработанныйЭлемент.Валидный Тогда
12+
ВременныйМассив.Добавить(ОбработанныйЭлемент);
13+
14+
Если ОбработанныйЭлемент.Приоритет > 10 Тогда
15+
Перем СпециальнаяОбработка = ВыполнитьСпециальнуюОбработку(ОбработанныйЭлемент);
16+
Результат.Вставить("Важный", СпециальнаяОбработка);
17+
ИначеЕсли ОбработанныйЭлемент.Приоритет > 5 Тогда
18+
Результат.Вставить("Средний", ОбработанныйЭлемент);
19+
Иначе
20+
Результат.Вставить("Обычный", ОбработанныйЭлемент);
21+
КонецЕсли;
22+
23+
Индекс = Индекс + 1;
24+
25+
Если Индекс > 1000 Тогда
26+
Прервать;
27+
КонецЕсли;
28+
КонецЕсли;
29+
КонецЕсли;
30+
КонецЦикла;
31+
32+
Результат.Вставить("ОбщееКоличество", Индекс);
33+
Результат.Вставить("Массив", ВременныйМассив);
34+
35+
Исключение
36+
Перем СообщениеОбОшибке = ОписаниеОшибки();
37+
Результат.Вставить("Ошибка", СообщениеОбОшибке);
38+
КонецПопытки;
39+
40+
Возврат Результат;
41+
КонецПроцедуры
42+
43+
Функция ОбработатьЭлемент(Элемент)
44+
Перем Результат = Новый Структура;
45+
46+
Результат.Вставить("Валидный", Истина);
47+
Результат.Вставить("Приоритет", 0);
48+
Результат.Вставить("Значение", Неопределено);
49+
50+
Если ТипЗнч(Элемент) = Тип("Структура") Тогда
51+
Результат.Вставить("Значение", Элемент.Значение);
52+
Результат.Вставить("Приоритет", Элемент.Приоритет);
53+
54+
Если НЕ (Элемент.Свойство("Значение") И Элемент.Свойство("Приоритет")) Тогда
55+
Результат.Вставить("Валидный", Ложь);
56+
КонецЕсли;
57+
ИначеЕсли ТипЗнч(Элемент) = Тип("Число") Тогда
58+
Результат.Вставить("Значение", Элемент);
59+
Результат.Вставить("Приоритет", 5);
60+
Иначе
61+
Результат.Вставить("Валидный", Ложь);
62+
КонецЕсли;
63+
64+
Возврат Результат;
65+
КонецФункции
66+
67+
Функция ВыполнитьСпециальнуюОбработку(Элемент)
68+
Перем Результат = Новый Структура;
69+
Перем ВременнаяПеременная = 0;
70+
71+
Для Счетчик = 1 По 10 Цикл
72+
ВременнаяПеременная = ВременнаяПеременная + Элемент.Значение * Счетчик;
73+
74+
Если Счетчик % 2 = 0 Тогда
75+
ВременнаяПеременная = ВременнаяПеременная / 2;
76+
КонецЕсли;
77+
КонецЦикла;
78+
79+
Результат.Вставить("ОбработанноеЗначение", ВременнаяПеременная);
80+
Результат.Вставить("ИсходноеЗначение", Элемент.Значение);
81+
Результат.Вставить("ИсходныйПриоритет", Элемент.Приоритет);
82+
83+
Возврат Результат;
84+
КонецФункции
85+
86+
Процедура ВспомогательнаяФункция1()
87+
Перем а = 1;
88+
Перем б = 2;
89+
Перем в = а + б;
90+
Перем г = а * б;
91+
Перем д = в / г;
92+
КонецПроцедуры
93+
94+
Процедура ВспомогательнаяФункция2(Параметр1, Знач Параметр2)
95+
Если Параметр1 > Параметр2 Тогда
96+
Возврат Параметр1;
97+
ИначеЕсли Параметр1 < Параметр2 Тогда
98+
Возврат Параметр2;
99+
Иначе
100+
Возврат 0;
101+
КонецЕсли;
102+
КонецПроцедуры

benchmarks/modules/medium.bsl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Процедура ОбработкаДанных(МассивДанных)
2+
Перем Счетчик = 0;
3+
Перем Сумма = 0;
4+
5+
Для Каждого Элемент Из МассивДанных Цикл
6+
Если Элемент.Значение <> Неопределено Тогда
7+
Сумма = Сумма + Элемент.Значение;
8+
Счетчик = Счетчик + 1;
9+
КонецЕсли;
10+
КонецЦикла;
11+
12+
Если Счетчик > 0 Тогда
13+
Возврат Сумма / Счетчик;
14+
Иначе
15+
Возврат 0;
16+
КонецЕсли;
17+
КонецПроцедуры
18+
19+
Функция ВычислитьСреднее(Массив)
20+
Перем Результат = 0;
21+
Для Индекс = 0 По Массив.ВГраница() Цикл
22+
Результат = Результат + Массив[Индекс];
23+
КонецЦикла;
24+
Возврат Результат / Массив.Количество();
25+
КонецФункции

benchmarks/modules/small.bsl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Процедура ПростойМетод()
2+
Перем а = 1;
3+
Перем б = 2;
4+
Возврат а + б;
5+
КонецПроцедуры

0 commit comments

Comments
 (0)