Skip to content

Commit 7d2bb8e

Browse files
author
Ruben Bridgewater
committed
Better pipelining
Add fallback mode
1 parent 9ee1e3c commit 7d2bb8e

File tree

3 files changed

+104
-90
lines changed

3 files changed

+104
-90
lines changed

README.md

Lines changed: 51 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ Redis. The interface in `node_redis` is to return an individual `Batch` object b
493493
The only difference between .batch and .multi is that no transaction is going to be used.
494494
Be aware that the errors are - just like in multi statements - in the result. Otherwise both, errors and results could be returned at the same time.
495495

496-
If you fire many commands at once this is going to boost the execution speed significantly (see the benchmark section). Please remember that all commands are kept in memory until they are fired.
496+
If you fire many commands at once this is going to **boost the execution speed by up to 400%** [sic!] compared to fireing the same commands in a loop without waiting for the result! See the benchmarks for further comparison. Please remember that all commands are kept in memory until they are fired.
497497

498498
## Monitor mode
499499

@@ -637,55 +637,56 @@ Here are results of `multi_bench.js` which is similar to `redis-benchmark` from
637637

638638
hiredis parser (Lenovo T450s i7-5600U):
639639

640-
Client count: 5, node version: 4.1.1, server version: 3.0.3, parser: hiredis
641-
PING, 1/5 min/max/avg/p95: 0/ 11/ 0.03/ 0.00 1412ms total, 35410.76 ops/sec
642-
PING, 50/5 min/max/avg/p95: 0/ 9/ 0.54/ 1.00 539ms total, 92764.38 ops/sec
643-
PING, batch 50/5 min/max/avg/p95: 0/ 3/ 0.32/ 1.00 327ms total, 152905.20 ops/sec
644-
SET 4B str, 1/5 min/max/avg/p95: 0/ 4/ 0.03/ 0.00 1450ms total, 34482.76 ops/sec
645-
SET 4B str, 50/5 min/max/avg/p95: 0/ 2/ 0.55/ 1.00 548ms total, 91240.88 ops/sec
646-
SET 4B str, batch 50/5 min/max/avg/p95: 0/ 10/ 0.36/ 1.00 362ms total, 138121.55 ops/sec
647-
SET 4B buf, 1/5 min/max/avg/p95: 0/ 5/ 0.06/ 0.55 2838ms total, 17618.04 ops/sec
648-
SET 4B buf, 50/5 min/max/avg/p95: 0/ 9/ 1.70/ 3.00 1699ms total, 29429.08 ops/sec
649-
SET 4B buf, batch 50/5 min/max/avg/p95: 1/ 11/ 1.69/ 3.00 1694ms total, 29515.94 ops/sec
650-
GET 4B str, 1/5 min/max/avg/p95: 0/ 4/ 0.03/ 0.00 1350ms total, 37037.04 ops/sec
651-
GET 4B str, 50/5 min/max/avg/p95: 0/ 7/ 0.54/ 1.00 539ms total, 92764.38 ops/sec
652-
GET 4B str, batch 50/5 min/max/avg/p95: 0/ 2/ 0.48/ 1.00 483ms total, 103519.67 ops/sec
653-
GET 4B buf, 1/5 min/max/avg/p95: 0/ 9/ 0.03/ 0.00 1373ms total, 36416.61 ops/sec
654-
GET 4B buf, 50/5 min/max/avg/p95: 0/ 2/ 0.53/ 1.00 534ms total, 93632.96 ops/sec
655-
GET 4B buf, batch 50/5 min/max/avg/p95: 0/ 10/ 0.60/ 1.00 605ms total, 82644.63 ops/sec
656-
SET 4KiB str, 1/5 min/max/avg/p95: 0/ 5/ 0.03/ 0.00 1790ms total, 27932.96 ops/sec
657-
SET 4KiB str, 50/5 min/max/avg/p95: 0/ 7/ 0.80/ 2.00 798ms total, 62656.64 ops/sec
658-
SET 4KiB str, batch 50/5 min/max/avg/p95: 0/ 10/ 0.92/ 1.00 924ms total, 54112.55 ops/sec
659-
SET 4KiB buf, 1/5 min/max/avg/p95: 0/ 16/ 0.05/ 1.00 2687ms total, 18608.11 ops/sec
660-
SET 4KiB buf, 50/5 min/max/avg/p95: 0/ 16/ 1.88/ 3.00 1885ms total, 26525.20 ops/sec
661-
SET 4KiB buf, batch 50/5 min/max/avg/p95: 1/ 6/ 1.83/ 3.00 1832ms total, 27292.58 ops/sec
662-
GET 4KiB str, 1/5 min/max/avg/p95: 0/ 7/ 0.04/ 0.00 1909ms total, 26191.72 ops/sec
663-
GET 4KiB str, 50/5 min/max/avg/p95: 0/ 8/ 0.88/ 2.00 887ms total, 56369.79 ops/sec
664-
GET 4KiB str, batch 50/5 min/max/avg/p95: 0/ 4/ 0.57/ 1.00 570ms total, 87719.30 ops/sec
665-
GET 4KiB buf, 1/5 min/max/avg/p95: 0/ 7/ 0.03/ 0.00 1754ms total, 28506.27 ops/sec
666-
GET 4KiB buf, 50/5 min/max/avg/p95: 0/ 6/ 0.72/ 1.00 717ms total, 69735.01 ops/sec
667-
GET 4KiB buf, batch 50/5 min/max/avg/p95: 0/ 1/ 0.47/ 1.00 472ms total, 105932.20 ops/sec
668-
INCR, 1/5 min/max/avg/p95: 0/ 8/ 0.03/ 0.00 1531ms total, 32658.39 ops/sec
669-
INCR, 50/5 min/max/avg/p95: 0/ 5/ 0.64/ 1.00 638ms total, 78369.91 ops/sec
670-
INCR, batch 50/5 min/max/avg/p95: 0/ 13/ 0.45/ 1.00 452ms total, 110619.47 ops/sec
671-
LPUSH, 1/5 min/max/avg/p95: 0/ 4/ 0.03/ 0.00 1445ms total, 34602.08 ops/sec
672-
LPUSH, 50/5 min/max/avg/p95: 0/ 9/ 0.67/ 1.00 670ms total, 74626.87 ops/sec
673-
LPUSH, batch 50/5 min/max/avg/p95: 0/ 2/ 0.34/ 1.00 339ms total, 147492.63 ops/sec
674-
LRANGE 10, 1/5 min/max/avg/p95: 0/ 9/ 0.03/ 0.00 1739ms total, 28752.16 ops/sec
675-
LRANGE 10, 50/5 min/max/avg/p95: 0/ 11/ 0.76/ 2.00 759ms total, 65876.15 ops/sec
676-
LRANGE 10, batch 50/5 min/max/avg/p95: 0/ 4/ 0.49/ 1.00 497ms total, 100603.62 ops/sec
677-
LRANGE 100, 1/5 min/max/avg/p95: 0/ 7/ 0.06/ 1.00 3252ms total, 15375.15 ops/sec
678-
LRANGE 100, 50/5 min/max/avg/p95: 0/ 9/ 1.90/ 3.00 1905ms total, 26246.72 ops/sec
679-
LRANGE 100, batch 50/5 min/max/avg/p95: 1/ 5/ 1.81/ 2.00 1816ms total, 27533.04 ops/sec
680-
SET 4MiB buf, 1/5 min/max/avg/p95: 2/ 5/ 2.32/ 3.00 1160ms total, 431.03 ops/sec
681-
SET 4MiB buf, 50/5 min/max/avg/p95: 19/ 134/ 102.27/ 118.00 1071ms total, 466.85 ops/sec
682-
SET 4MiB buf, batch 50/5 min/max/avg/p95: 97/ 129/ 104.90/ 129.00 1049ms total, 476.64 ops/sec
683-
GET 4MiB str, 1/5 min/max/avg/p95: 4/ 19/ 6.59/ 11.00 660ms total, 151.52 ops/sec
684-
GET 4MiB str, 50/5 min/max/avg/p95: 19/ 278/ 200.11/ 258.85 503ms total, 198.81 ops/sec
685-
GET 4MiB str, batch 50/5 min/max/avg/p95: 229/ 235/ 232.00/ 235.00 465ms total, 215.05 ops/sec
686-
GET 4MiB buf, 1/5 min/max/avg/p95: 4/ 27/ 7.11/ 13.95 713ms total, 140.25 ops/sec
687-
GET 4MiB buf, 50/5 min/max/avg/p95: 7/ 293/ 204.74/ 269.00 518ms total, 193.05 ops/sec
688-
GET 4MiB buf, batch 50/5 min/max/avg/p95: 219/ 261/ 240.00/ 261.00 480ms total, 208.33 ops/sec
640+
Client count: 5, node version: 4.1.2, server version: 3.0.3, parser: hiredis
641+
PING, 1/5 min/max/avg/p95: 0/ 5/ 0.03/ 0.00 1537ms total, 32530.90 ops/sec
642+
PING, 50/5 min/max/avg/p95: 0/ 4/ 0.49/ 1.00 491ms total, 101832.99 ops/sec
643+
PING, batch 50/5 min/max/avg/p95: 0/ 2/ 0.17/ 1.00 178ms total, 280898.88 ops/sec
644+
SET 4B str, 1/5 min/max/avg/p95: 0/ 2/ 0.03/ 0.00 1400ms total, 35714.29 ops/sec
645+
SET 4B str, 50/5 min/max/avg/p95: 0/ 3/ 0.61/ 1.00 610ms total, 81967.21 ops/sec
646+
SET 4B str, batch 50/5 min/max/avg/p95: 0/ 1/ 0.19/ 1.00 198ms total, 252525.25 ops/sec
647+
SET 4B buf, 1/5 min/max/avg/p95: 0/ 3/ 0.05/ 0.00 2349ms total, 21285.65 ops/sec
648+
SET 4B buf, 50/5 min/max/avg/p95: 0/ 5/ 1.63/ 3.00 1632ms total, 30637.25 ops/sec
649+
SET 4B buf, batch 50/5 min/max/avg/p95: 0/ 1/ 0.37/ 1.00 366ms total, 136612.02 ops/sec
650+
GET 4B str, 1/5 min/max/avg/p95: 0/ 3/ 0.03/ 0.00 1348ms total, 37091.99 ops/sec
651+
GET 4B str, 50/5 min/max/avg/p95: 0/ 3/ 0.51/ 1.00 513ms total, 97465.89 ops/sec
652+
GET 4B str, batch 50/5 min/max/avg/p95: 0/ 1/ 0.18/ 1.00 177ms total, 282485.88 ops/sec
653+
GET 4B buf, 1/5 min/max/avg/p95: 0/ 3/ 0.03/ 0.00 1336ms total, 37425.15 ops/sec
654+
GET 4B buf, 50/5 min/max/avg/p95: 0/ 4/ 0.52/ 1.00 525ms total, 95238.10 ops/sec
655+
GET 4B buf, batch 50/5 min/max/avg/p95: 0/ 1/ 0.18/ 1.00 177ms total, 282485.88 ops/sec
656+
SET 4KiB str, 1/5 min/max/avg/p95: 0/ 2/ 0.03/ 0.00 1674ms total, 29868.58 ops/sec
657+
SET 4KiB str, 50/5 min/max/avg/p95: 0/ 3/ 0.77/ 1.00 775ms total, 64516.13 ops/sec
658+
SET 4KiB str, batch 50/5 min/max/avg/p95: 0/ 3/ 0.50/ 1.00 500ms total, 100000.00 ops/sec
659+
SET 4KiB buf, 1/5 min/max/avg/p95: 0/ 2/ 0.05/ 0.00 2410ms total, 20746.89 ops/sec
660+
SET 4KiB buf, 50/5 min/max/avg/p95: 0/ 5/ 1.64/ 3.00 1643ms total, 30432.14 ops/sec
661+
SET 4KiB buf, batch 50/5 min/max/avg/p95: 0/ 1/ 0.41/ 1.00 409ms total, 122249.39 ops/sec
662+
GET 4KiB str, 1/5 min/max/avg/p95: 0/ 2/ 0.03/ 0.00 1422ms total, 35161.74 ops/sec
663+
GET 4KiB str, 50/5 min/max/avg/p95: 0/ 4/ 0.68/ 1.00 680ms total, 73529.41 ops/sec
664+
GET 4KiB str, batch 50/5 min/max/avg/p95: 0/ 2/ 0.39/ 1.00 391ms total, 127877.24 ops/sec
665+
GET 4KiB buf, 1/5 min/max/avg/p95: 0/ 1/ 0.03/ 0.00 1420ms total, 35211.27 ops/sec
666+
GET 4KiB buf, 50/5 min/max/avg/p95: 0/ 4/ 0.68/ 1.00 681ms total, 73421.44 ops/sec
667+
GET 4KiB buf, batch 50/5 min/max/avg/p95: 0/ 2/ 0.39/ 1.00 387ms total, 129198.97 ops/sec
668+
INCR, 1/5 min/max/avg/p95: 0/ 2/ 0.03/ 0.00 1334ms total, 37481.26 ops/sec
669+
INCR, 50/5 min/max/avg/p95: 0/ 4/ 0.51/ 1.00 513ms total, 97465.89 ops/sec
670+
INCR, batch 50/5 min/max/avg/p95: 0/ 1/ 0.18/ 1.00 179ms total, 279329.61 ops/sec
671+
LPUSH, 1/5 min/max/avg/p95: 0/ 2/ 0.03/ 0.00 1351ms total, 37009.62 ops/sec
672+
LPUSH, 50/5 min/max/avg/p95: 0/ 3/ 0.52/ 1.00 521ms total, 95969.29 ops/sec
673+
LPUSH, batch 50/5 min/max/avg/p95: 0/ 2/ 0.20/ 1.00 200ms total, 250000.00 ops/sec
674+
LRANGE 10, 1/5 min/max/avg/p95: 0/ 1/ 0.03/ 0.00 1562ms total, 32010.24 ops/sec
675+
LRANGE 10, 50/5 min/max/avg/p95: 0/ 4/ 0.69/ 1.00 690ms total, 72463.77 ops/sec
676+
LRANGE 10, batch 50/5 min/max/avg/p95: 0/ 2/ 0.39/ 1.00 393ms total, 127226.46 ops/sec
677+
LRANGE 100, 1/5 min/max/avg/p95: 0/ 3/ 0.06/ 1.00 3009ms total, 16616.82 ops/sec
678+
LRANGE 100, 50/5 min/max/avg/p95: 0/ 5/ 1.85/ 3.00 1850ms total, 27027.03 ops/sec
679+
LRANGE 100, batch 50/5 min/max/avg/p95: 2/ 4/ 2.15/ 3.00 2153ms total, 23223.41 ops/sec
680+
SET 4MiB buf, 1/5 min/max/avg/p95: 1/ 5/ 1.91/ 3.00 957ms total, 522.47 ops/sec
681+
SET 4MiB buf, 50/5 min/max/avg/p95: 13/ 109/ 94.20/ 102.00 987ms total, 506.59 ops/sec
682+
SET 4MiB buf, batch 50/5 min/max/avg/p95: 90/ 107/ 93.10/ 107.00 931ms total, 537.06 ops/sec
683+
GET 4MiB str, 1/5 min/max/avg/p95: 4/ 16/ 5.97/ 10.00 598ms total, 167.22 ops/sec
684+
GET 4MiB str, 50/5 min/max/avg/p95: 10/ 249/ 179.47/ 231.90 454ms total, 220.26 ops/sec
685+
GET 4MiB str, batch 50/5 min/max/avg/p95: 215/ 226/ 220.50/ 226.00 441ms total, 226.76 ops/sec
686+
GET 4MiB buf, 1/5 min/max/avg/p95: 3/ 26/ 6.55/ 11.95 658ms total, 151.98 ops/sec
687+
GET 4MiB buf, 50/5 min/max/avg/p95: 11/ 265/ 186.73/ 241.90 469ms total, 213.22 ops/sec
688+
GET 4MiB buf, batch 50/5 min/max/avg/p95: 226/ 247/ 236.50/ 247.00 473ms total, 211.42 ops/sec
689+
End of tests. Total time elapsed: 44952 ms
689690

690691
The hiredis and js parser should most of the time be on the same level. The js parser lacks speed for large responses though.
691692
Therefor the hiredis parser is the default used in node_redis. To use `hiredis`, do:

changelog.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
11
Changelog
22
=========
33

4-
## v.2.2.0 - 07, 2015 - The peregrino falcon
4+
## v.2.2.0 - 08, 2015 - The peregrino falcon
5+
6+
The peregrino falcon is the fasted bird on earth and this is what this release is all about: We increased performance for heavy usage by up to **400%** [sic!] and increased overall performance for any command as well. Please check the benchmarks in the [README.md](README.md) for further details.
57

68
Features
79

8-
- Added disable_resubscribing option to prevent a client from resubscribing after reconnecting (@BridgeAR)
9-
- Added rename_commands options to handle renamed commands from the redis config (@digmxl & @BridgeAR)
10-
- Increased performance (@BridgeAR)
11-
- exchanging built in queue with [Petka Antonov's](@petkaantonov) [double-ended queue](https://github.com/petkaantonov/deque)
10+
- Added rename_commands options to handle renamed commands from the redis config ([@digmxl](https://github.com/digmxl) & [@BridgeAR](https://github.com/BridgeAR))
11+
- Added disable_resubscribing option to prevent a client from resubscribing after reconnecting ([@BridgeAR](https://github.com/BridgeAR))
12+
- Increased performance ([@BridgeAR](https://github.com/BridgeAR))
13+
- exchanging built in queue with [@petkaantonov](https://github.com/petkaantonov)'s [double-ended queue](https://github.com/petkaantonov/deque)
1214
- prevent polymorphism
1315
- optimize statements
14-
- Added .batch command, similar to multi but without transaction (@BridgeAR)
15-
- Improved pipelining to minimize the [RTT](http://redis.io/topics/pipelining) further (@BridgeAR)
16+
- Added *.batch* command, similar to .multi but without transaction ([@BridgeAR](https://github.com/BridgeAR))
17+
- Improved pipelining to minimize the [RTT](http://redis.io/topics/pipelining) further ([@BridgeAR](https://github.com/BridgeAR))
18+
19+
Bugfixes
1620

17-
This release is mainly focusing on further speed improvements and we can proudly say that node_redis is very likely outperforming any other node redis client.
21+
- Fix a javascript parser regression introduced in 2.0 that could result in timeouts on high load. ([@BridgeAR](https://github.com/BridgeAR))
22+
- Fixed should_buffer boolean for .exec, .select and .auth commands not being returned ([@BridgeAR](https://github.com/BridgeAR))
1823

19-
If you do not rely on transactions but want to reduze the RTT you can use .batch from now on. It'll behave just the same as .multi but it does not have any transaction and therefor won't roll back any failed commands.
20-
Both .multi and .batch are from now on going to fire the commands in bulk without doing any other operation in between.
24+
If you do not rely on transactions but want to reduce the RTT you can use .batch from now on. It'll behave just the same as .multi but it does not have any transaction and therefor won't roll back any failed commands.<br>
25+
Both .multi and .batch are from now on going to cache the commands and release them while calling .exec.
2126

22-
Bugfixes
27+
Please consider using .batch instead of looping through a lot of commands one by one. This will significantly improve your performance.
2328

24-
- Fix a javascript parser regression introduced in 2.0 that could result in timeouts on high load. (@BridgeAR)
25-
- Fixed should_buffer boolean for .exec, .select and .auth commands not being returned (@BridgeAR)
29+
To conclude: we can proudly say that node_redis is very likely outperforming any other node redis client.
2630

2731
## v2.1.0 - Oct 02, 2015
2832

index.js

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ parsers.push(require('./lib/parsers/javascript'));
3535
function RedisClient(stream, options) {
3636
options = options || {};
3737

38+
if (!stream.cork) {
39+
stream.cork = function noop() {};
40+
stream.uncork = function noop() {};
41+
stream.__write = stream.write;
42+
stream.write = this.writeStream.bind(this);
43+
}
44+
3845
this.stream = stream;
3946
this.options = options;
4047

@@ -650,26 +657,6 @@ RedisClient.prototype.return_reply = function (reply) {
650657
}
651658
};
652659

653-
RedisClient.prototype.writeStream = function (data) {
654-
var stream = this.stream;
655-
var nr = 0;
656-
657-
// Do not use a pipeline
658-
if (this.pipeline === 0) {
659-
return !stream.write(data);
660-
}
661-
this.pipeline--;
662-
this.pipeline_queue.push(data);
663-
if (this.pipeline === 0) {
664-
var len = this.pipeline_queue.length;
665-
while (len--) {
666-
nr += !stream.write(this.pipeline_queue.shift());
667-
}
668-
return !nr;
669-
}
670-
return true;
671-
};
672-
673660
RedisClient.prototype.send_command = function (command, args, callback) {
674661
var arg, command_obj, i, err,
675662
stream = this.stream,
@@ -775,29 +762,29 @@ RedisClient.prototype.send_command = function (command, args, callback) {
775762
command_str += '$' + Buffer.byteLength(arg) + '\r\n' + arg + '\r\n';
776763
}
777764
debug('Send ' + this.address + ' id ' + this.connection_id + ': ' + command_str);
778-
buffered_writes += !this.writeStream(command_str);
765+
buffered_writes += !stream.write(command_str);
779766
} else {
780767
debug('Send command (' + command_str + ') has Buffer arguments');
781-
buffered_writes += !this.writeStream(command_str);
768+
buffered_writes += !stream.write(command_str);
782769

783770
for (i = 0; i < args.length; i += 1) {
784771
arg = args[i];
785772
if (Buffer.isBuffer(arg)) {
786773
if (arg.length === 0) {
787774
debug('send_command: using empty string for 0 length buffer');
788-
buffered_writes += !this.writeStream('$0\r\n\r\n');
775+
buffered_writes += !stream.write('$0\r\n\r\n');
789776
} else {
790-
buffered_writes += !this.writeStream('$' + arg.length + '\r\n');
791-
buffered_writes += !this.writeStream(arg);
792-
buffered_writes += !this.writeStream('\r\n');
777+
buffered_writes += !stream.write('$' + arg.length + '\r\n');
778+
buffered_writes += !stream.write(arg);
779+
buffered_writes += !stream.write('\r\n');
793780
debug('send_command: buffer send ' + arg.length + ' bytes');
794781
}
795782
} else {
796783
if (typeof arg !== 'string') {
797784
arg = String(arg);
798785
}
799786
debug('send_command: string send ' + Buffer.byteLength(arg) + ' bytes: ' + arg);
800-
buffered_writes += !this.writeStream('$' + Buffer.byteLength(arg) + '\r\n' + arg + '\r\n');
787+
buffered_writes += !stream.write('$' + Buffer.byteLength(arg) + '\r\n' + arg + '\r\n');
801788
}
802789
}
803790
}
@@ -808,6 +795,25 @@ RedisClient.prototype.send_command = function (command, args, callback) {
808795
return !this.should_buffer;
809796
};
810797

798+
RedisClient.prototype.writeStream = function (data) {
799+
var nr = 0;
800+
801+
// Do not use a pipeline
802+
if (this.pipeline === 0) {
803+
return !this.stream.__write(data);
804+
}
805+
this.pipeline--;
806+
this.pipeline_queue.push(data);
807+
if (this.pipeline === 0) {
808+
var len = this.pipeline_queue.length;
809+
while (len--) {
810+
nr += !this.stream.__write(this.pipeline_queue.shift());
811+
}
812+
return !nr;
813+
}
814+
return true;
815+
};
816+
811817
RedisClient.prototype.pub_sub_command = function (command_obj) {
812818
var i, key, command, args;
813819

@@ -862,6 +868,7 @@ RedisClient.prototype.end = function (flush) {
862868
};
863869

864870
function Multi(client, args, transaction) {
871+
client.stream.cork();
865872
this._client = client;
866873
this.queue = [];
867874
if (transaction) {
@@ -1091,6 +1098,7 @@ Multi.prototype.exec_transaction = function (callback) {
10911098
this.send_command(command, args, index, cb);
10921099
}
10931100

1101+
this._client.stream.uncork();
10941102
return this._client.send_command('exec', [], function(err, replies) {
10951103
self.execute_callback(err, replies);
10961104
});
@@ -1198,6 +1206,7 @@ Multi.prototype.exec = Multi.prototype.EXEC = function (callback) {
11981206
this._client.send_command(command, args, cb);
11991207
index++;
12001208
}
1209+
this._client.stream.uncork();
12011210
return this._client.should_buffer;
12021211
};
12031212

0 commit comments

Comments
 (0)