xref: /redis-3.2.3/src/latency.c (revision f3fd58eb)
1 /* The latency monitor allows to easily observe the sources of latency
2  * in a Redis instance using the LATENCY command. Different latency
3  * sources are monitored, like disk I/O, execution of commands, fork
4  * system call, and so forth.
5  *
6  * ----------------------------------------------------------------------------
7  *
8  * Copyright (c) 2014, Salvatore Sanfilippo <antirez at gmail dot com>
9  * All rights reserved.
10  *
11  * Redistribution and use in source and binary forms, with or without
12  * modification, are permitted provided that the following conditions are met:
13  *
14  *   * Redistributions of source code must retain the above copyright notice,
15  *     this list of conditions and the following disclaimer.
16  *   * Redistributions in binary form must reproduce the above copyright
17  *     notice, this list of conditions and the following disclaimer in the
18  *     documentation and/or other materials provided with the distribution.
19  *   * Neither the name of Redis nor the names of its contributors may be used
20  *     to endorse or promote products derived from this software without
21  *     specific prior written permission.
22  *
23  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
27  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33  * POSSIBILITY OF SUCH DAMAGE.
34  */
35 
36 #include "redis.h"
37 
38 /* Dictionary type for latency events. */
39 int dictStringKeyCompare(void *privdata, const void *key1, const void *key2) {
40     REDIS_NOTUSED(privdata);
41     return strcmp(key1,key2) == 0;
42 }
43 
44 unsigned int dictStringHash(const void *key) {
45     return dictGenHashFunction(key, strlen(key));
46 }
47 
48 void dictVanillaFree(void *privdata, void *val);
49 
50 dictType latencyTimeSeriesDictType = {
51     dictStringHash,             /* hash function */
52     NULL,                       /* key dup */
53     NULL,                       /* val dup */
54     dictStringKeyCompare,       /* key compare */
55     dictVanillaFree,            /* key destructor */
56     dictVanillaFree             /* val destructor */
57 };
58 
59 /* ------------------------- Utility functions ------------------------------ */
60 
61 #ifdef __linux__
62 /* Returns 1 if Transparent Huge Pages support is enabled in the kernel.
63  * Otherwise (or if we are unable to check) 0 is returned. */
64 int THPIsEnabled(void) {
65     char buf[1024];
66 
67     FILE *fp = fopen("/sys/kernel/mm/transparent_hugepage/enabled","r");
68     if (!fp) return 0;
69     if (fgets(buf,sizeof(buf),fp) == NULL) {
70         fclose(fp);
71         return 0;
72     }
73     fclose(fp);
74     return (strstr(buf,"[never]") == NULL) ? 1 : 0;
75 }
76 #endif
77 
78 /* Report the amount of AnonHugePages in smap, in bytes. If the return
79  * value of the function is non-zero, the process is being targeted by
80  * THP support, and is likely to have memory usage / latency issues. */
81 int THPGetAnonHugePagesSize(void) {
82     return zmalloc_get_smap_bytes_by_field("AnonHugePages:");
83 }
84 
85 /* ---------------------------- Latency API --------------------------------- */
86 
87 /* Latency monitor initialization. We just need to create the dictionary
88  * of time series, each time serie is craeted on demand in order to avoid
89  * having a fixed list to maintain. */
90 void latencyMonitorInit(void) {
91     server.latency_events = dictCreate(&latencyTimeSeriesDictType,NULL);
92 }
93 
94 /* Add the specified sample to the specified time series "event".
95  * This function is usually called via latencyAddSampleIfNeeded(), that
96  * is a macro that only adds the sample if the latency is higher than
97  * server.latency_monitor_threshold. */
98 void latencyAddSample(char *event, mstime_t latency) {
99     struct latencyTimeSeries *ts = dictFetchValue(server.latency_events,event);
100     time_t now = time(NULL);
101     int prev;
102 
103     /* Create the time series if it does not exist. */
104     if (ts == NULL) {
105         ts = zmalloc(sizeof(*ts));
106         ts->idx = 0;
107         ts->max = 0;
108         memset(ts->samples,0,sizeof(ts->samples));
109         dictAdd(server.latency_events,zstrdup(event),ts);
110     }
111 
112     /* If the previous sample is in the same second, we update our old sample
113      * if this latency is > of the old one, or just return. */
114     prev = (ts->idx + LATENCY_TS_LEN - 1) % LATENCY_TS_LEN;
115     if (ts->samples[prev].time == now) {
116         if (latency > ts->samples[prev].latency)
117             ts->samples[prev].latency = latency;
118         return;
119     }
120 
121     ts->samples[ts->idx].time = time(NULL);
122     ts->samples[ts->idx].latency = latency;
123     if (latency > ts->max) ts->max = latency;
124 
125     ts->idx++;
126     if (ts->idx == LATENCY_TS_LEN) ts->idx = 0;
127 }
128 
129 /* Reset data for the specified event, or all the events data if 'event' is
130  * NULL.
131  *
132  * Note: this is O(N) even when event_to_reset is not NULL because makes
133  * the code simpler and we have a small fixed max number of events. */
134 int latencyResetEvent(char *event_to_reset) {
135     dictIterator *di;
136     dictEntry *de;
137     int resets = 0;
138 
139     di = dictGetSafeIterator(server.latency_events);
140     while((de = dictNext(di)) != NULL) {
141         char *event = dictGetKey(de);
142 
143         if (event_to_reset == NULL || strcasecmp(event,event_to_reset) == 0) {
144             dictDelete(server.latency_events, event);
145             resets++;
146         }
147     }
148     dictReleaseIterator(di);
149     return resets;
150 }
151 
152 /* ------------------------ Latency reporting (doctor) ---------------------- */
153 
154 /* Analyze the samples avaialble for a given event and return a structure
155  * populate with different metrics, average, MAD, min, max, and so forth.
156  * Check latency.h definition of struct latenctStat for more info.
157  * If the specified event has no elements the structure is populate with
158  * zero values. */
159 void analyzeLatencyForEvent(char *event, struct latencyStats *ls) {
160     struct latencyTimeSeries *ts = dictFetchValue(server.latency_events,event);
161     int j;
162     uint64_t sum;
163 
164     ls->all_time_high = ts ? ts->max : 0;
165     ls->avg = 0;
166     ls->min = 0;
167     ls->max = 0;
168     ls->mad = 0;
169     ls->samples = 0;
170     ls->period = 0;
171     if (!ts) return;
172 
173     /* First pass, populate everything but the MAD. */
174     sum = 0;
175     for (j = 0; j < LATENCY_TS_LEN; j++) {
176         if (ts->samples[j].time == 0) continue;
177         ls->samples++;
178         if (ls->samples == 1) {
179             ls->min = ls->max = ts->samples[j].latency;
180         } else {
181             if (ls->min > ts->samples[j].latency)
182                 ls->min = ts->samples[j].latency;
183             if (ls->max < ts->samples[j].latency)
184                 ls->max = ts->samples[j].latency;
185         }
186         sum += ts->samples[j].latency;
187 
188         /* Track the oldest event time in ls->period. */
189         if (ls->period == 0 || ts->samples[j].time < ls->period)
190             ls->period = ts->samples[j].time;
191     }
192 
193     /* So far avg is actually the sum of the latencies, and period is
194      * the oldest event time. We need to make the first an average and
195      * the second a range of seconds. */
196     if (ls->samples) {
197         ls->avg = sum / ls->samples;
198         ls->period = time(NULL) - ls->period;
199         if (ls->period == 0) ls->period = 1;
200     }
201 
202     /* Second pass, compute MAD. */
203     sum = 0;
204     for (j = 0; j < LATENCY_TS_LEN; j++) {
205         int64_t delta;
206 
207         if (ts->samples[j].time == 0) continue;
208         delta = (int64_t)ls->avg - ts->samples[j].latency;
209         if (delta < 0) delta = -delta;
210         sum += delta;
211     }
212     if (ls->samples) ls->mad = sum / ls->samples;
213 }
214 
215 /* Create a human readable report of latency events for this Redis instance. */
216 sds createLatencyReport(void) {
217     sds report = sdsempty();
218     int advise_better_vm = 0;       /* Better virtual machines. */
219     int advise_slowlog_enabled = 0; /* Enable slowlog. */
220     int advise_slowlog_tuning = 0;  /* Reconfigure slowlog. */
221     int advise_slowlog_inspect = 0; /* Check your slowlog. */
222     int advise_disk_contention = 0; /* Try to lower disk contention. */
223     int advise_scheduler = 0;       /* Intrinsic latency. */
224     int advise_data_writeback = 0;  /* data=writeback. */
225     int advise_no_appendfsync = 0;  /* don't fsync during rewrites. */
226     int advise_local_disk = 0;      /* Avoid remote disks. */
227     int advise_ssd = 0;             /* Use an SSD drive. */
228     int advise_write_load_info = 0; /* Print info about AOF and write load. */
229     int advise_hz = 0;              /* Use higher HZ. */
230     int advise_large_objects = 0;   /* Deletion of large objects. */
231     int advise_relax_fsync_policy = 0; /* appendfsync always is slow. */
232     int advise_disable_thp = 0;     /* AnonHugePages detected. */
233     int advices = 0;
234 
235     /* Return ASAP if the latency engine is disabled and it looks like it
236      * was never enabled so far. */
237     if (dictSize(server.latency_events) == 0 &&
238         server.latency_monitor_threshold == 0)
239     {
240         report = sdscat(report,"I'm sorry, Dave, I can't do that. Latency monitoring is disabled in this Redis instance. You may use \"CONFIG SET latency-monitor-threshold <milliseconds>.\" in order to enable it. If we weren't in a deep space mission I'd suggest to take a look at http://redis.io/topics/latency-monitor.\n");
241         return report;
242     }
243 
244     /* Show all the events stats and add for each event some event-related
245      * comment depending on the values. */
246     dictIterator *di;
247     dictEntry *de;
248     int eventnum = 0;
249 
250     di = dictGetIterator(server.latency_events);
251     while((de = dictNext(di)) != NULL) {
252         char *event = dictGetKey(de);
253         struct latencyTimeSeries *ts = dictGetVal(de);
254         struct latencyStats ls;
255 
256         if (ts == NULL) continue;
257         eventnum++;
258         if (eventnum == 1) {
259             report = sdscat(report,"Dave, I have observed latency spikes in this Redis instance. You don't mind talking about it, do you Dave?\n\n");
260         }
261         analyzeLatencyForEvent(event,&ls);
262 
263         report = sdscatprintf(report,
264             "%d. %s: %d latency spikes (average %lums, mean deviation %lums, period %.2f sec). Worst all time event %lums.",
265             eventnum, event,
266             ls.samples,
267             (unsigned long) ls.avg,
268             (unsigned long) ls.mad,
269             (double) ls.period/ls.samples,
270             (unsigned long) ts->max);
271 
272         /* Fork */
273         if (!strcasecmp(event,"fork")) {
274             char *fork_quality;
275             if (server.stat_fork_rate < 10) {
276                 fork_quality = "terrible";
277                 advise_better_vm = 1;
278                 advices++;
279             } else if (server.stat_fork_rate < 25) {
280                 fork_quality = "poor";
281                 advise_better_vm = 1;
282                 advices++;
283             } else if (server.stat_fork_rate < 100) {
284                 fork_quality = "good";
285             } else {
286                 fork_quality = "excellent";
287             }
288             report = sdscatprintf(report,
289                 " Fork rate is %.2f GB/sec (%s).", server.stat_fork_rate,
290                 fork_quality);
291         }
292 
293         /* Potentially commands. */
294         if (!strcasecmp(event,"command")) {
295             if (server.slowlog_log_slower_than == 0) {
296                 advise_slowlog_enabled = 1;
297                 advices++;
298             } else if (server.slowlog_log_slower_than/1000 >
299                        server.latency_monitor_threshold)
300             {
301                 advise_slowlog_tuning = 1;
302                 advices++;
303             }
304             advise_slowlog_inspect = 1;
305             advise_large_objects = 1;
306             advices += 2;
307         }
308 
309         /* fast-command. */
310         if (!strcasecmp(event,"fast-command")) {
311             advise_scheduler = 1;
312             advices++;
313         }
314 
315         /* AOF and I/O. */
316         if (!strcasecmp(event,"aof-write-pending-fsync")) {
317             advise_local_disk = 1;
318             advise_disk_contention = 1;
319             advise_ssd = 1;
320             advise_data_writeback = 1;
321             advices += 4;
322         }
323 
324         if (!strcasecmp(event,"aof-write-active-child")) {
325             advise_no_appendfsync = 1;
326             advise_data_writeback = 1;
327             advise_ssd = 1;
328             advices += 3;
329         }
330 
331         if (!strcasecmp(event,"aof-write-alone")) {
332             advise_local_disk = 1;
333             advise_data_writeback = 1;
334             advise_ssd = 1;
335             advices += 3;
336         }
337 
338         if (!strcasecmp(event,"aof-fsync-always")) {
339             advise_relax_fsync_policy = 1;
340             advices++;
341         }
342 
343         if (!strcasecmp(event,"aof-fstat") ||
344             !strcasecmp(event,"rdb-unlik-temp-file")) {
345             advise_disk_contention = 1;
346             advise_local_disk = 1;
347             advices += 2;
348         }
349 
350         if (!strcasecmp(event,"aof-rewrite-diff-write") ||
351             !strcasecmp(event,"aof-rename")) {
352             advise_write_load_info = 1;
353             advise_data_writeback = 1;
354             advise_ssd = 1;
355             advise_local_disk = 1;
356             advices += 4;
357         }
358 
359         /* Expire cycle. */
360         if (!strcasecmp(event,"expire-cycle")) {
361             advise_hz = 1;
362             advise_large_objects = 1;
363             advices += 2;
364         }
365 
366         /* Eviction cycle. */
367         if (!strcasecmp(event,"eviction-cycle")) {
368             advise_large_objects = 1;
369             advices++;
370         }
371 
372         report = sdscatlen(report,"\n",1);
373     }
374     dictReleaseIterator(di);
375 
376     /* Add non event based advices. */
377     if (THPGetAnonHugePagesSize() > 0) {
378         advise_disable_thp = 1;
379         advices++;
380     }
381 
382     if (eventnum == 0 && advices == 0) {
383         report = sdscat(report,"Dave, no latency spike was observed during the lifetime of this Redis instance, not in the slightest bit. I honestly think you ought to sit down calmly, take a stress pill, and think things over.\n");
384     } else if (eventnum > 0 && advices == 0) {
385         report = sdscat(report,"\nWhile there are latency events logged, I'm not able to suggest any easy fix. Please use the Redis community to get some help, providing this report in your help request.\n");
386     } else {
387         /* Add all the suggestions accumulated so far. */
388 
389         /* Better VM. */
390         report = sdscat(report,"\nI have a few advices for you:\n\n");
391         if (advise_better_vm) {
392             report = sdscat(report,"- If you are using a virtual machine, consider upgrading it with a faster one using an hypervisior that provides less latency during fork() calls. Xen is known to have poor fork() performance. Even in the context of the same VM provider, certain kinds of instances can execute fork faster than others.\n");
393         }
394 
395         /* Slow log. */
396         if (advise_slowlog_enabled) {
397             report = sdscatprintf(report,"- There are latency issues with potentially slow commands you are using. Try to enable the Slow Log Redis feature using the command 'CONFIG SET slowlog-log-slower-than %llu'. If the Slow log is disabled Redis is not able to log slow commands execution for you.\n", (unsigned long long)server.latency_monitor_threshold*1000);
398         }
399 
400         if (advise_slowlog_tuning) {
401             report = sdscatprintf(report,"- Your current Slow Log configuration only logs events that are slower than your configured latency monitor threshold. Please use 'CONFIG SET slowlog-log-slower-than %llu'.\n", (unsigned long long)server.latency_monitor_threshold*1000);
402         }
403 
404         if (advise_slowlog_inspect) {
405             report = sdscat(report,"- Check your Slow Log to understand what are the commands you are running which are too slow to execute. Please check http://redis.io/commands/slowlog for more information.\n");
406         }
407 
408         /* Intrinsic latency. */
409         if (advise_scheduler) {
410             report = sdscat(report,"- The system is slow to execute Redis code paths not containing system calls. This usually means the system does not provide Redis CPU time to run for long periods. You should try to:\n"
411             "  1) Lower the system load.\n"
412             "  2) Use a computer / VM just for Redis if you are running other softawre in the same system.\n"
413             "  3) Check if you have a \"noisy neighbour\" problem.\n"
414             "  4) Check with 'redis-cli --intrinsic-latency 100' what is the intrinsic latency in your system.\n"
415             "  5) Check if the problem is allocator-related by recompiling Redis with MALLOC=libc, if you are using Jemalloc. However this may create fragmentation problems.\n");
416         }
417 
418         /* AOF / Disk latency. */
419         if (advise_local_disk) {
420             report = sdscat(report,"- It is strongly advised to use local disks for persistence, especially if you are using AOF. Remote disks provided by platform-as-a-service providers are known to be slow.\n");
421         }
422 
423         if (advise_ssd) {
424             report = sdscat(report,"- SSD disks are able to reduce fsync latency, and total time needed for snapshotting and AOF log rewriting (resulting in smaller memory usage and smaller final AOF rewrite buffer flushes). With extremely high write load SSD disks can be a good option. However Redis should perform reasonably with high load using normal disks. Use this advice as a last resort.\n");
425         }
426 
427         if (advise_data_writeback) {
428             report = sdscat(report,"- Mounting ext3/4 filesystems with data=writeback can provide a performance boost compared to data=ordered, however this mode of operation provides less guarantees, and sometimes it can happen that after a hard crash the AOF file will have an half-written command at the end and will require to be repaired before Redis restarts.\n");
429         }
430 
431         if (advise_disk_contention) {
432             report = sdscat(report,"- Try to lower the disk contention. This is often caused by other disk intensive processes running in the same computer (including other Redis instances).\n");
433         }
434 
435         if (advise_no_appendfsync) {
436             report = sdscat(report,"- Assuming from the point of view of data safety this is viable in your environment, you could try to enable the 'no-appendfsync-on-rewrite' option, so that fsync will not be performed while there is a child rewriting the AOF file or producing an RDB file (the moment where there is high disk contention).\n");
437         }
438 
439         if (advise_relax_fsync_policy && server.aof_fsync == AOF_FSYNC_ALWAYS) {
440             report = sdscat(report,"- Your fsync policy is set to 'always'. It is very hard to get good performances with such a setup, if possible try to relax the fsync policy to 'onesec'.\n");
441         }
442 
443         if (advise_write_load_info) {
444             report = sdscat(report,"- Latency during the AOF atomic rename operation or when the final difference is flushed to the AOF file at the end of the rewrite, sometimes is caused by very high write load, causing the AOF buffer to get very large. If possible try to send less commands to accomplish the same work, or use Lua scripts to group multiple operations into a single EVALSHA call.\n");
445         }
446 
447         if (advise_hz && server.hz < 100) {
448             report = sdscat(report,"- In order to make the Redis keys expiring process more incremental, try to set the 'hz' configuration parameter to 100 using 'CONFIG SET hz 100'.\n");
449         }
450 
451         if (advise_large_objects) {
452             report = sdscat(report,"- Deleting, expiring or evicting (because of maxmemory policy) large objects is a blocking operation. If you have very large objects that are often deleted, expired, or evicted, try to fragment those objects into multiple smaller objects.\n");
453         }
454 
455         if (advise_disable_thp) {
456             report = sdscat(report,"- I detected a non zero amount of anonymous huge pages used by your process. This creates very serious latency events in different conditions, especially when Redis is persisting on disk. To disable THP support use the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled', make sure to also add it into /etc/rc.local so that the command will be executed again after a reboot. Note that even if you have already disabled THP, you still need to restart the Redis process to get rid of the huge pages already created.\n");
457         }
458     }
459 
460     return report;
461 }
462 
463 /* ---------------------- Latency command implementation -------------------- */
464 
465 /* latencyCommand() helper to produce a time-delay reply for all the samples
466  * in memory for the specified time series. */
467 void latencyCommandReplyWithSamples(redisClient *c, struct latencyTimeSeries *ts) {
468     void *replylen = addDeferredMultiBulkLength(c);
469     int samples = 0, j;
470 
471     for (j = 0; j < LATENCY_TS_LEN; j++) {
472         int i = (ts->idx + j) % LATENCY_TS_LEN;
473 
474         if (ts->samples[i].time == 0) continue;
475         addReplyMultiBulkLen(c,2);
476         addReplyLongLong(c,ts->samples[i].time);
477         addReplyLongLong(c,ts->samples[i].latency);
478         samples++;
479     }
480     setDeferredMultiBulkLength(c,replylen,samples);
481 }
482 
483 /* latencyCommand() helper to produce the reply for the LATEST subcommand,
484  * listing the last latency sample for every event type registered so far. */
485 void latencyCommandReplyWithLatestEvents(redisClient *c) {
486     dictIterator *di;
487     dictEntry *de;
488 
489     addReplyMultiBulkLen(c,dictSize(server.latency_events));
490     di = dictGetIterator(server.latency_events);
491     while((de = dictNext(di)) != NULL) {
492         char *event = dictGetKey(de);
493         struct latencyTimeSeries *ts = dictGetVal(de);
494         int last = (ts->idx + LATENCY_TS_LEN - 1) % LATENCY_TS_LEN;
495 
496         addReplyMultiBulkLen(c,4);
497         addReplyBulkCString(c,event);
498         addReplyLongLong(c,ts->samples[last].time);
499         addReplyLongLong(c,ts->samples[last].latency);
500         addReplyLongLong(c,ts->max);
501     }
502     dictReleaseIterator(di);
503 }
504 
505 #define LATENCY_GRAPH_COLS 80
506 sds latencyCommandGenSparkeline(char *event, struct latencyTimeSeries *ts) {
507     int j;
508     struct sequence *seq = createSparklineSequence();
509     sds graph = sdsempty();
510     uint32_t min = 0, max = 0;
511 
512     for (j = 0; j < LATENCY_TS_LEN; j++) {
513         int i = (ts->idx + j) % LATENCY_TS_LEN;
514         int elapsed;
515         char buf[64];
516 
517         if (ts->samples[i].time == 0) continue;
518         /* Update min and max. */
519         if (seq->length == 0) {
520             min = max = ts->samples[i].latency;
521         } else {
522             if (ts->samples[i].latency > max) max = ts->samples[i].latency;
523             if (ts->samples[i].latency < min) min = ts->samples[i].latency;
524         }
525         /* Use as label the number of seconds / minutes / hours / days
526          * ago the event happened. */
527         elapsed = time(NULL) - ts->samples[i].time;
528         if (elapsed < 60)
529             snprintf(buf,sizeof(buf),"%ds",elapsed);
530         else if (elapsed < 3600)
531             snprintf(buf,sizeof(buf),"%dm",elapsed/60);
532         else if (elapsed < 3600*24)
533             snprintf(buf,sizeof(buf),"%dh",elapsed/3600);
534         else
535             snprintf(buf,sizeof(buf),"%dd",elapsed/(3600*24));
536         sparklineSequenceAddSample(seq,ts->samples[i].latency,buf);
537     }
538 
539     graph = sdscatprintf(graph,
540         "%s - high %lu ms, low %lu ms (all time high %lu ms)\n", event,
541         (unsigned long) max, (unsigned long) min, (unsigned long) ts->max);
542     for (j = 0; j < LATENCY_GRAPH_COLS; j++)
543         graph = sdscatlen(graph,"-",1);
544     graph = sdscatlen(graph,"\n",1);
545     graph = sparklineRender(graph,seq,LATENCY_GRAPH_COLS,4,SPARKLINE_FILL);
546     freeSparklineSequence(seq);
547     return graph;
548 }
549 
550 /* LATENCY command implementations.
551  *
552  * LATENCY SAMPLES: return time-latency samples for the specified event.
553  * LATENCY LATEST: return the latest latency for all the events classes.
554  * LATENCY DOCTOR: returns an human readable analysis of instance latency.
555  * LATENCY GRAPH: provide an ASCII graph of the latency of the specified event.
556  */
557 void latencyCommand(redisClient *c) {
558     struct latencyTimeSeries *ts;
559 
560     if (!strcasecmp(c->argv[1]->ptr,"history") && c->argc == 3) {
561         /* LATENCY HISTORY <event> */
562         ts = dictFetchValue(server.latency_events,c->argv[2]->ptr);
563         if (ts == NULL) {
564             addReplyMultiBulkLen(c,0);
565         } else {
566             latencyCommandReplyWithSamples(c,ts);
567         }
568     } else if (!strcasecmp(c->argv[1]->ptr,"graph") && c->argc == 3) {
569         /* LATENCY GRAPH <event> */
570         sds graph;
571         dictEntry *de;
572         char *event;
573 
574         de = dictFind(server.latency_events,c->argv[2]->ptr);
575         if (de == NULL) goto nodataerr;
576         ts = dictGetVal(de);
577         event = dictGetKey(de);
578 
579         graph = latencyCommandGenSparkeline(event,ts);
580         addReplyBulkCString(c,graph);
581         sdsfree(graph);
582     } else if (!strcasecmp(c->argv[1]->ptr,"latest") && c->argc == 2) {
583         /* LATENCY LATEST */
584         latencyCommandReplyWithLatestEvents(c);
585     } else if (!strcasecmp(c->argv[1]->ptr,"doctor") && c->argc == 2) {
586         /* LATENCY DOCTOR */
587         sds report = createLatencyReport();
588 
589         addReplyBulkCBuffer(c,report,sdslen(report));
590         sdsfree(report);
591     } else if (!strcasecmp(c->argv[1]->ptr,"reset") && c->argc >= 2) {
592         /* LATENCY RESET */
593         if (c->argc == 2) {
594             addReplyLongLong(c,latencyResetEvent(NULL));
595         } else {
596             int j, resets = 0;
597 
598             for (j = 2; j < c->argc; j++)
599                 resets += latencyResetEvent(c->argv[j]->ptr);
600             addReplyLongLong(c,resets);
601         }
602     } else {
603         addReply(c,shared.syntaxerr);
604     }
605     return;
606 
607 nodataerr:
608     /* Common error when the user asks for an event we have no latency
609      * information about. */
610     addReplyErrorFormat(c,
611         "No samples available for event '%s'", (char*) c->argv[2]->ptr);
612 }
613 
614