ollama-bench 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/bun.lockb +0 -0
- package/dist/index.js +143 -0
- package/package.json +31 -0
- package/src/index.ts +181 -0
- package/tsconfig.json +15 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 dalist.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# ollama-benchmark
|
|
2
|
+
|
|
3
|
+
A command-line tool to benchmark Ollama models performance.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js >= 14.0.0
|
|
8
|
+
- Ollama installed and running on your system
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
You can install globally:
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g ollama-bench
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or run directly with npx:
|
|
18
|
+
```bash
|
|
19
|
+
npx ollama-bench <model1> [model2] [model3] ...
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
ollama-bench mistral llama2 codellama
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This will:
|
|
29
|
+
1. Pull the specified models if not already present
|
|
30
|
+
2. Run a benchmark test on each model
|
|
31
|
+
3. Compare and display the results
|
|
32
|
+
4. Show the best performing model
|
|
33
|
+
|
|
34
|
+
## Output
|
|
35
|
+
|
|
36
|
+
The tool provides real-time feedback with:
|
|
37
|
+
- Loading animations during operations
|
|
38
|
+
- Color-coded status messages
|
|
39
|
+
- Detailed benchmark results
|
|
40
|
+
- Comparison of model performance
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT
|
package/bun.lockb
ADDED
|
Binary file
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import ollama from 'ollama';
|
|
3
|
+
/**
|
|
4
|
+
* Object containing ANSI color codes for text coloring.
|
|
5
|
+
*/
|
|
6
|
+
const colors = {
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
green: '\x1b[32m',
|
|
9
|
+
yellow: '\x1b[33m',
|
|
10
|
+
red: '\x1b[31m',
|
|
11
|
+
cyan: '\x1b[36m',
|
|
12
|
+
magenta: '\x1b[35m',
|
|
13
|
+
blue: '\x1b[34m',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Object containing emoji characters for various status indicators.
|
|
17
|
+
*/
|
|
18
|
+
const emojis = {
|
|
19
|
+
rocket: '🚀',
|
|
20
|
+
check: '✅',
|
|
21
|
+
error: '❌',
|
|
22
|
+
hourglass: '⏳',
|
|
23
|
+
star: '⭐',
|
|
24
|
+
trophy: '🏆',
|
|
25
|
+
gear: '⚙️',
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Applies color to the given text.
|
|
29
|
+
* @param text - The text to colorize.
|
|
30
|
+
* @param color - The color to apply.
|
|
31
|
+
* @returns The colorized text.
|
|
32
|
+
*/
|
|
33
|
+
function colorize(text, color) {
|
|
34
|
+
return `${colors[color]}${text}${colors.reset}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates a loading animation for the console.
|
|
38
|
+
* @param operation - The operation being performed.
|
|
39
|
+
* @param model - The model being processed.
|
|
40
|
+
* @returns An interval ID for the animation.
|
|
41
|
+
*/
|
|
42
|
+
function createLoadingAnimation(operation, model) {
|
|
43
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
44
|
+
let i = 0;
|
|
45
|
+
let dots = 0;
|
|
46
|
+
return setInterval(() => {
|
|
47
|
+
const frame = frames[i];
|
|
48
|
+
const dotString = '.'.repeat(dots);
|
|
49
|
+
const operationText = colorize(`${operation} ${model}${dotString}`, 'blue');
|
|
50
|
+
process.stdout.write(`\r${frame} ${emojis.gear} ${operationText}`.padEnd(50));
|
|
51
|
+
i = (i + 1) % frames.length;
|
|
52
|
+
dots = (dots + 1) % 4;
|
|
53
|
+
}, 100);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Pulls a model from Ollama.
|
|
57
|
+
* @param model - The name of the model to pull.
|
|
58
|
+
*/
|
|
59
|
+
async function pullModel(model) {
|
|
60
|
+
console.log(colorize(`${emojis.rocket} Initiating pull for ${model}...`, 'yellow'));
|
|
61
|
+
const loadingAnimation = createLoadingAnimation('Pulling', model);
|
|
62
|
+
try {
|
|
63
|
+
const start = performance.now();
|
|
64
|
+
const response = await ollama.pull({ model, stream: true });
|
|
65
|
+
for await (const part of response) {
|
|
66
|
+
if (part.status === 'success') {
|
|
67
|
+
clearInterval(loadingAnimation);
|
|
68
|
+
const end = performance.now();
|
|
69
|
+
const duration = (end - start) / 1000;
|
|
70
|
+
console.log(`\r${colorize(`${emojis.check} Successfully pulled ${model} in ${duration.toFixed(2)} seconds`, 'green')} `);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
clearInterval(loadingAnimation);
|
|
77
|
+
console.log(`\r${colorize(`${emojis.error} Error pulling ${model}: ${error.message}`, 'red')} `);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Benchmarks a model's performance.
|
|
82
|
+
* @param model - The name of the model to benchmark.
|
|
83
|
+
* @returns A promise that resolves to the benchmark result.
|
|
84
|
+
*/
|
|
85
|
+
async function benchmarkModel(model) {
|
|
86
|
+
const prompt = "Explain the theory of relativity in simple terms.";
|
|
87
|
+
console.log(colorize(`${emojis.hourglass} Initiating benchmark for ${model}...`, 'cyan'));
|
|
88
|
+
const loadingAnimation = createLoadingAnimation('Benchmarking', model);
|
|
89
|
+
try {
|
|
90
|
+
const response = await ollama.generate({
|
|
91
|
+
model,
|
|
92
|
+
prompt,
|
|
93
|
+
stream: false,
|
|
94
|
+
});
|
|
95
|
+
clearInterval(loadingAnimation);
|
|
96
|
+
const totalDuration = response.total_duration / 1e9; // Convert nanoseconds to seconds
|
|
97
|
+
const tokensPerSecond = response.eval_count / (response.eval_duration / 1e9);
|
|
98
|
+
console.log(`\r${colorize(`${emojis.star} Benchmark results for ${model}:`, 'cyan')} `);
|
|
99
|
+
console.log(colorize(` Total time: ${totalDuration.toFixed(2)} seconds`, 'yellow'));
|
|
100
|
+
console.log(colorize(` Tokens generated: ${response.eval_count}`, 'yellow'));
|
|
101
|
+
console.log(colorize(` Tokens per second: ${tokensPerSecond.toFixed(2)}`, 'yellow'));
|
|
102
|
+
console.log();
|
|
103
|
+
return { model, tokensPerSecond };
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
clearInterval(loadingAnimation);
|
|
107
|
+
console.log(`\r${colorize(`${emojis.error} Error benchmarking ${model}: ${error.message}`, 'red')} `);
|
|
108
|
+
return { model, tokensPerSecond: 0 };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The main function that orchestrates the model pulling and benchmarking process.
|
|
113
|
+
*/
|
|
114
|
+
export async function main() {
|
|
115
|
+
const models = process.argv.slice(2);
|
|
116
|
+
if (models.length === 0) {
|
|
117
|
+
console.log(colorize(`${emojis.error} Error: No models provided. Please specify at least one model.`, 'red'));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
console.log(colorize(`${emojis.rocket} Ollama Benchmark Script`, 'cyan'));
|
|
121
|
+
console.log(colorize("=======================", 'cyan'));
|
|
122
|
+
// Pull models
|
|
123
|
+
for (const model of models) {
|
|
124
|
+
await pullModel(model);
|
|
125
|
+
}
|
|
126
|
+
console.log();
|
|
127
|
+
// Benchmark models
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const model of models) {
|
|
130
|
+
const result = await benchmarkModel(model);
|
|
131
|
+
results.push(result);
|
|
132
|
+
}
|
|
133
|
+
// Find the best performing model
|
|
134
|
+
const bestModel = results.reduce((best, current) => current.tokensPerSecond > best.tokensPerSecond ? current : best);
|
|
135
|
+
console.log(colorize(`${emojis.trophy} Best performing model:`, 'magenta'));
|
|
136
|
+
console.log(colorize(` ${bestModel.model} with ${bestModel.tokensPerSecond.toFixed(2)} tokens/second`, 'magenta'));
|
|
137
|
+
}
|
|
138
|
+
if (import.meta.url === import.meta.resolve(process.argv[1])) {
|
|
139
|
+
main().catch(error => {
|
|
140
|
+
console.error('Error:', error);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
});
|
|
143
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ollama-bench",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A CLI tool to benchmark Ollama models performance",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ollama-benchmark": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsc && node dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": ["ollama", "benchmark", "ai", "models", "cli"],
|
|
16
|
+
"author": "dalist1",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"ollama": "latest"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.17.5",
|
|
23
|
+
"typescript": "^5.6.3"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=14.0.0"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import ollama from 'ollama';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents the available color codes for text coloring.
|
|
7
|
+
*/
|
|
8
|
+
type Color = 'reset' | 'green' | 'yellow' | 'red' | 'cyan' | 'magenta' | 'blue';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Represents the available emoji keys.
|
|
12
|
+
*/
|
|
13
|
+
type Emoji = 'rocket' | 'check' | 'error' | 'hourglass' | 'star' | 'trophy' | 'gear';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Object containing ANSI color codes for text coloring.
|
|
17
|
+
*/
|
|
18
|
+
const colors: Record<Color, string> = {
|
|
19
|
+
reset: '\x1b[0m',
|
|
20
|
+
green: '\x1b[32m',
|
|
21
|
+
yellow: '\x1b[33m',
|
|
22
|
+
red: '\x1b[31m',
|
|
23
|
+
cyan: '\x1b[36m',
|
|
24
|
+
magenta: '\x1b[35m',
|
|
25
|
+
blue: '\x1b[34m',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Object containing emoji characters for various status indicators.
|
|
30
|
+
*/
|
|
31
|
+
const emojis: Record<Emoji, string> = {
|
|
32
|
+
rocket: '🚀',
|
|
33
|
+
check: '✅',
|
|
34
|
+
error: '❌',
|
|
35
|
+
hourglass: '⏳',
|
|
36
|
+
star: '⭐',
|
|
37
|
+
trophy: '🏆',
|
|
38
|
+
gear: '⚙️',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Applies color to the given text.
|
|
43
|
+
* @param text - The text to colorize.
|
|
44
|
+
* @param color - The color to apply.
|
|
45
|
+
* @returns The colorized text.
|
|
46
|
+
*/
|
|
47
|
+
function colorize(text: string, color: Color): string {
|
|
48
|
+
return `${colors[color]}${text}${colors.reset}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a loading animation for the console.
|
|
53
|
+
* @param operation - The operation being performed.
|
|
54
|
+
* @param model - The model being processed.
|
|
55
|
+
* @returns An interval ID for the animation.
|
|
56
|
+
*/
|
|
57
|
+
function createLoadingAnimation(operation: string, model: string): NodeJS.Timeout {
|
|
58
|
+
const frames: string[] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
59
|
+
let i = 0;
|
|
60
|
+
let dots = 0;
|
|
61
|
+
return setInterval(() => {
|
|
62
|
+
const frame = frames[i];
|
|
63
|
+
const dotString = '.'.repeat(dots);
|
|
64
|
+
const operationText = colorize(`${operation} ${model}${dotString}`, 'blue');
|
|
65
|
+
process.stdout.write(`\r${frame} ${emojis.gear} ${operationText}`.padEnd(50));
|
|
66
|
+
i = (i + 1) % frames.length;
|
|
67
|
+
dots = (dots + 1) % 4;
|
|
68
|
+
}, 100);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Pulls a model from Ollama.
|
|
73
|
+
* @param model - The name of the model to pull.
|
|
74
|
+
*/
|
|
75
|
+
async function pullModel(model: string): Promise<void> {
|
|
76
|
+
console.log(colorize(`${emojis.rocket} Initiating pull for ${model}...`, 'yellow'));
|
|
77
|
+
const loadingAnimation = createLoadingAnimation('Pulling', model);
|
|
78
|
+
try {
|
|
79
|
+
const start = performance.now();
|
|
80
|
+
const response = await ollama.pull({ model, stream: true });
|
|
81
|
+
for await (const part of response) {
|
|
82
|
+
if (part.status === 'success') {
|
|
83
|
+
clearInterval(loadingAnimation);
|
|
84
|
+
const end = performance.now();
|
|
85
|
+
const duration = (end - start) / 1000;
|
|
86
|
+
console.log(`\r${colorize(`${emojis.check} Successfully pulled ${model} in ${duration.toFixed(2)} seconds`, 'green')} `);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
clearInterval(loadingAnimation);
|
|
92
|
+
console.log(`\r${colorize(`${emojis.error} Error pulling ${model}: ${(error as Error).message}`, 'red')} `);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Represents the result of a model benchmark.
|
|
98
|
+
*/
|
|
99
|
+
interface BenchmarkResult {
|
|
100
|
+
model: string;
|
|
101
|
+
tokensPerSecond: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Benchmarks a model's performance.
|
|
106
|
+
* @param model - The name of the model to benchmark.
|
|
107
|
+
* @returns A promise that resolves to the benchmark result.
|
|
108
|
+
*/
|
|
109
|
+
async function benchmarkModel(model: string): Promise<BenchmarkResult> {
|
|
110
|
+
const prompt = "Explain the theory of relativity in simple terms.";
|
|
111
|
+
console.log(colorize(`${emojis.hourglass} Initiating benchmark for ${model}...`, 'cyan'));
|
|
112
|
+
const loadingAnimation = createLoadingAnimation('Benchmarking', model);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const response = await ollama.generate({
|
|
116
|
+
model,
|
|
117
|
+
prompt,
|
|
118
|
+
stream: false,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
clearInterval(loadingAnimation);
|
|
122
|
+
const totalDuration = response.total_duration / 1e9; // Convert nanoseconds to seconds
|
|
123
|
+
const tokensPerSecond = response.eval_count / (response.eval_duration / 1e9);
|
|
124
|
+
|
|
125
|
+
console.log(`\r${colorize(`${emojis.star} Benchmark results for ${model}:`, 'cyan')} `);
|
|
126
|
+
console.log(colorize(` Total time: ${totalDuration.toFixed(2)} seconds`, 'yellow'));
|
|
127
|
+
console.log(colorize(` Tokens generated: ${response.eval_count}`, 'yellow'));
|
|
128
|
+
console.log(colorize(` Tokens per second: ${tokensPerSecond.toFixed(2)}`, 'yellow'));
|
|
129
|
+
console.log();
|
|
130
|
+
|
|
131
|
+
return { model, tokensPerSecond };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
clearInterval(loadingAnimation);
|
|
134
|
+
console.log(`\r${colorize(`${emojis.error} Error benchmarking ${model}: ${(error as Error).message}`, 'red')} `);
|
|
135
|
+
return { model, tokensPerSecond: 0 };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The main function that orchestrates the model pulling and benchmarking process.
|
|
141
|
+
*/
|
|
142
|
+
export async function main(): Promise<void> {
|
|
143
|
+
const models = process.argv.slice(2);
|
|
144
|
+
|
|
145
|
+
if (models.length === 0) {
|
|
146
|
+
console.log(colorize(`${emojis.error} Error: No models provided. Please specify at least one model.`, 'red'));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(colorize(`${emojis.rocket} Ollama Benchmark Script`, 'cyan'));
|
|
151
|
+
console.log(colorize("=======================", 'cyan'));
|
|
152
|
+
|
|
153
|
+
// Pull models
|
|
154
|
+
for (const model of models) {
|
|
155
|
+
await pullModel(model);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log();
|
|
159
|
+
|
|
160
|
+
// Benchmark models
|
|
161
|
+
const results: BenchmarkResult[] = [];
|
|
162
|
+
for (const model of models) {
|
|
163
|
+
const result = await benchmarkModel(model);
|
|
164
|
+
results.push(result);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Find the best performing model
|
|
168
|
+
const bestModel = results.reduce((best, current) =>
|
|
169
|
+
current.tokensPerSecond > best.tokensPerSecond ? current : best
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
console.log(colorize(`${emojis.trophy} Best performing model:`, 'magenta'));
|
|
173
|
+
console.log(colorize(` ${bestModel.model} with ${bestModel.tokensPerSecond.toFixed(2)} tokens/second`, 'magenta'));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (import.meta.url === import.meta.resolve(process.argv[1])) {
|
|
177
|
+
main().catch(error => {
|
|
178
|
+
console.error('Error:', error);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
});
|
|
181
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ES2020",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|