1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 /*
22  * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
23  * Use is subject to license terms.
24  */
25 
26 #include <stdio.h>
27 #include <stdlib.h>
28 #include <strings.h>
29 #include <err.h>
30 #include <errno.h>
31 #include <kstat.h>
32 #include <unistd.h>
33 #include <signal.h>
34 #include <sys/dld.h>
35 
36 #include <libdllink.h>
37 #include <libdlflow.h>
38 #include <libdlstat.h>
39 
40 /*
41  * x86 <sys/regs> ERR conflicts with <curses.h> ERR.
42  * Include curses.h last.
43  */
44 #if	defined(ERR)
45 #undef  ERR
46 #endif
47 #include <curses.h>
48 
49 struct flowlist {
50 	char		flowname[MAXFLOWNAMELEN];
51 	datalink_id_t	linkid;
52 	uint64_t	ifspeed;
53 	boolean_t	first;
54 	boolean_t	display;
55 	pktsum_t 	prevstats;
56 	pktsum_t	diffstats;
57 };
58 
59 static	int	maxx, maxy, redraw = 0;
60 static	volatile uint_t handle_resize = 0, handle_break = 0;
61 
62 pktsum_t		totalstats;
63 struct flowlist		*stattable = NULL;
64 static int		statentry = -1, maxstatentries = 0;
65 
66 #define	STATGROWSIZE	16
67 
68 
69 /*
70  * Search for flowlist entry in stattable which matches
71  * the flowname and linkide.  If no match is found, use
72  * next available slot.  If no slots are available,
73  * reallocate table with  more slots.
74  *
75  * Return: *flowlist of matching flow
76  *         NULL if realloc fails
77  */
78 
79 static struct flowlist *
80 findstat(const char *flowname, datalink_id_t linkid)
81 {
82 	int match = 0;
83 	struct flowlist *flist;
84 
85 	/* Look for match in the stattable */
86 	for (match = 0, flist = stattable;
87 	    match <= statentry;
88 	    match++, flist++) {
89 
90 		if (flist == NULL)
91 			break;
92 		/* match the flowname */
93 		if (flowname != NULL) {
94 			if (strncmp(flowname, flist->flowname, MAXFLOWNAMELEN)
95 			    == NULL)
96 				return (flist);
97 		/* match the linkid */
98 		} else {
99 			if (linkid == flist->linkid)
100 				return (flist);
101 		}
102 	}
103 
104 	/*
105 	 * No match found in the table.  Store statistics in the next slot.
106 	 * If necessary, make room for this entry.
107 	 */
108 	statentry++;
109 	if ((maxstatentries == 0) || (maxstatentries == statentry)) {
110 		maxstatentries += STATGROWSIZE;
111 		stattable = realloc(stattable,
112 		    maxstatentries * sizeof (struct flowlist));
113 		if (stattable == NULL) {
114 			perror("realloc");
115 			return (struct flowlist *)(NULL);
116 		}
117 	}
118 	flist = &stattable[statentry];
119 	bzero(flist, sizeof (struct flowlist));
120 	flist->first = B_TRUE;
121 
122 	if (flowname != NULL)
123 		(void) strncpy(flist->flowname, flowname, MAXFLOWNAMELEN);
124 	flist->linkid = linkid;
125 	return (flist);
126 }
127 
128 static void
129 print_flow_stats(dladm_handle_t handle, struct flowlist *flist)
130 {
131 	struct flowlist *fcurr;
132 	double ikbs, okbs;
133 	double ipks, opks;
134 	double dlt;
135 	int fcount;
136 	static boolean_t first = B_TRUE;
137 
138 	if (first) {
139 		first = B_FALSE;
140 		(void) printw("please wait...\n");
141 		return;
142 	}
143 
144 	for (fcount = 0, fcurr = flist;
145 	    fcount <= statentry;
146 	    fcount++, fcurr++) {
147 		if (fcurr->flowname && fcurr->display) {
148 			char linkname[MAXLINKNAMELEN];
149 
150 			(void) dladm_datalink_id2info(handle, fcurr->linkid,
151 			    NULL, NULL, NULL, linkname, sizeof (linkname));
152 			dlt = (double)fcurr->diffstats.snaptime/(double)NANOSEC;
153 			ikbs = fcurr->diffstats.rbytes * 8 / dlt / 1024;
154 			okbs = fcurr->diffstats.obytes * 8 / dlt / 1024;
155 			ipks = fcurr->diffstats.ipackets / dlt;
156 			opks = fcurr->diffstats.opackets / dlt;
157 			(void) printw("%-15.15s", fcurr->flowname);
158 			(void) printw("%-10.10s", linkname);
159 			(void) printw("%9.2f %9.2f %9.2f %9.2f ",
160 			    ikbs, okbs, ipks, opks);
161 			(void) printw("\n");
162 		}
163 	}
164 }
165 
166 /*ARGSUSED*/
167 static int
168 flow_kstats(dladm_flow_attr_t *attr, void *arg)
169 {
170 	kstat_ctl_t 	*kcp = (kstat_ctl_t *)arg;
171 	kstat_t		*ksp;
172 	struct flowlist	*flist;
173 	pktsum_t	currstats, *prevstats, *diffstats;
174 
175 	flist = findstat(attr->fa_flowname, attr->fa_linkid);
176 	if (flist != NULL) {
177 		prevstats = &flist->prevstats;
178 		diffstats = &flist->diffstats;
179 	} else {
180 		return (DLADM_STATUS_FAILED);
181 	}
182 
183 	/* lookup kstat entry */
184 	ksp = dladm_kstat_lookup(kcp, NULL, -1, attr->fa_flowname, "flow");
185 
186 	if (ksp == NULL)
187 		return (DLADM_WALK_TERMINATE);
188 	else
189 		flist->display = B_TRUE;
190 
191 	dladm_get_stats(kcp, ksp, &currstats);
192 	if (flist->ifspeed == 0)
193 		(void) dladm_kstat_value(ksp, "ifspeed", KSTAT_DATA_UINT64,
194 		    &flist->ifspeed);
195 
196 	if (flist->first)
197 		flist->first = B_FALSE;
198 	else {
199 		dladm_stats_diff(diffstats, &currstats, prevstats);
200 		dladm_stats_total(&totalstats, diffstats, &totalstats);
201 	}
202 
203 	bcopy(&currstats, prevstats, sizeof (pktsum_t));
204 	return (DLADM_WALK_CONTINUE);
205 }
206 
207 static void
208 print_link_stats(dladm_handle_t handle, struct flowlist *flist)
209 {
210 	struct flowlist *fcurr;
211 	double ikbs, okbs;
212 	double ipks, opks;
213 	double util;
214 	double dlt;
215 	int fcount;
216 	static boolean_t first = B_TRUE;
217 
218 	if (first) {
219 		first = B_FALSE;
220 		(void) printw("please wait...\n");
221 		return;
222 	}
223 
224 	for (fcount = 0, fcurr = flist;
225 	    fcount <= statentry;
226 	    fcount++, fcurr++) {
227 		if ((fcurr->linkid != DATALINK_INVALID_LINKID) &&
228 		    fcurr->display)  {
229 			char linkname[MAXLINKNAMELEN];
230 
231 			(void) dladm_datalink_id2info(handle, fcurr->linkid,
232 			    NULL, NULL, NULL, linkname, sizeof (linkname));
233 			dlt = (double)fcurr->diffstats.snaptime/(double)NANOSEC;
234 			ikbs = (double)fcurr->diffstats.rbytes * 8 / dlt / 1024;
235 			okbs = (double)fcurr->diffstats.obytes * 8 / dlt / 1024;
236 			ipks = (double)fcurr->diffstats.ipackets / dlt;
237 			opks = (double)fcurr->diffstats.opackets / dlt;
238 			(void) printw("%-10.10s", linkname);
239 			(void) printw("%9.2f %9.2f %9.2f %9.2f ",
240 			    ikbs, okbs, ipks, opks);
241 			if (fcurr->ifspeed != 0)
242 				util = ((ikbs + okbs) * 1024) *
243 				    100/ fcurr->ifspeed;
244 			else
245 				util = (double)0;
246 			(void) attron(A_BOLD);
247 			(void) printw("    %6.2f", util);
248 			(void) attroff(A_BOLD);
249 			(void) printw("\n");
250 		}
251 	}
252 }
253 
254 /*
255  * This function is called through the dladm_walk_datalink_id() walker and
256  * calls the dladm_walk_flow() walker.
257  */
258 
259 /*ARGSUSED*/
260 static int
261 link_flowstats(dladm_handle_t handle, datalink_id_t linkid, void *arg)
262 {
263 	dladm_status_t	status;
264 
265 	status = dladm_walk_flow(flow_kstats, handle, linkid, arg, B_FALSE);
266 	if (status == DLADM_STATUS_OK)
267 		return (DLADM_WALK_CONTINUE);
268 	else
269 		return (DLADM_WALK_TERMINATE);
270 }
271 
272 /*ARGSUSED*/
273 static int
274 link_kstats(dladm_handle_t handle, datalink_id_t linkid, void *arg)
275 {
276 	kstat_ctl_t	*kcp = (kstat_ctl_t *)arg;
277 	struct flowlist	*flist;
278 	pktsum_t	currstats, *prevstats, *diffstats;
279 	kstat_t		*ksp;
280 	char		linkname[MAXLINKNAMELEN];
281 
282 	/* find the flist entry */
283 	flist = findstat(NULL, linkid);
284 	if (flist != NULL) {
285 		prevstats = &flist->prevstats;
286 		diffstats = &flist->diffstats;
287 	} else {
288 		return (DLADM_WALK_CONTINUE);
289 	}
290 
291 	/* lookup kstat entry */
292 	(void) dladm_datalink_id2info(handle, linkid, NULL, NULL, NULL,
293 	    linkname, sizeof (linkname));
294 
295 	if (linkname == NULL) {
296 		warn("no linkname for linkid");
297 		return (DLADM_WALK_TERMINATE);
298 	}
299 
300 	ksp = dladm_kstat_lookup(kcp, NULL, -1, linkname, "net");
301 
302 	if (ksp == NULL)
303 		return (DLADM_WALK_TERMINATE);
304 	else
305 		flist->display = B_TRUE;
306 
307 	/* read packet and byte stats */
308 	dladm_get_stats(kcp, ksp, &currstats);
309 
310 	if (flist->ifspeed == 0)
311 		(void) dladm_kstat_value(ksp, "ifspeed", KSTAT_DATA_UINT64,
312 		    &flist->ifspeed);
313 
314 	if (flist->first == B_TRUE)
315 		flist->first = B_FALSE;
316 	else
317 		dladm_stats_diff(diffstats, &currstats, prevstats);
318 
319 	bcopy(&currstats, prevstats, sizeof (*prevstats));
320 
321 	return (DLADM_WALK_CONTINUE);
322 }
323 
324 /*ARGSUSED*/
325 static void
326 sig_break(int s)
327 {
328 	handle_break = 1;
329 }
330 
331 /*ARGSUSED*/
332 static void
333 sig_resize(int s)
334 {
335 	handle_resize = 1;
336 }
337 
338 static void
339 curses_init()
340 {
341 	maxx = maxx;	/* lint */
342 	maxy = maxy;	/* lint */
343 
344 	/* Install signal handlers */
345 	(void) signal(SIGINT,  sig_break);
346 	(void) signal(SIGQUIT, sig_break);
347 	(void) signal(SIGTERM, sig_break);
348 	(void) signal(SIGWINCH, sig_resize);
349 
350 	/* Initialize ncurses */
351 	(void) initscr();
352 	(void) cbreak();
353 	(void) noecho();
354 	(void) curs_set(0);
355 	timeout(0);
356 	getmaxyx(stdscr, maxy, maxx);
357 }
358 
359 static void
360 curses_fin()
361 {
362 	(void) printw("\n");
363 	(void) curs_set(1);
364 	(void) nocbreak();
365 	(void) endwin();
366 
367 	free(stattable);
368 }
369 
370 static void
371 stat_report(dladm_handle_t handle, kstat_ctl_t *kcp,  datalink_id_t linkid,
372     const char *flowname, int opt)
373 {
374 
375 	double dlt, ikbs, okbs, ipks, opks;
376 
377 	struct flowlist *fstable = stattable;
378 
379 	if ((opt != LINK_REPORT) && (opt != FLOW_REPORT))
380 		return;
381 
382 	/* Handle window resizes */
383 	if (handle_resize) {
384 		(void) endwin();
385 		(void) initscr();
386 		(void) cbreak();
387 		(void) noecho();
388 		(void) curs_set(0);
389 		timeout(0);
390 		getmaxyx(stdscr, maxy, maxx);
391 		redraw = 1;
392 		handle_resize = 0;
393 	}
394 
395 	/* Print title */
396 	(void) erase();
397 	(void) attron(A_BOLD);
398 	(void) move(0, 0);
399 	if (opt == FLOW_REPORT)
400 		(void) printw("%-15.15s", "Flow");
401 	(void) printw("%-10.10s", "Link");
402 	(void) printw("%9.9s %9.9s %9.9s %9.9s ",
403 	    "iKb/s", "oKb/s", "iPk/s", "oPk/s");
404 	if (opt == LINK_REPORT)
405 		(void) printw("    %6.6s", "%Util");
406 	(void) printw("\n");
407 	(void) attroff(A_BOLD);
408 
409 	(void) move(2, 0);
410 
411 	/* Print stats for each link or flow */
412 	bzero(&totalstats, sizeof (totalstats));
413 	if (opt == LINK_REPORT) {
414 		/* Display all links */
415 		if (linkid == DATALINK_ALL_LINKID) {
416 			(void) dladm_walk_datalink_id(link_kstats, handle,
417 			    (void *)kcp, DATALINK_CLASS_ALL,
418 			    DATALINK_ANY_MEDIATYPE, DLADM_OPT_ACTIVE);
419 		/* Display 1 link */
420 		} else {
421 			(void) link_kstats(handle, linkid, kcp);
422 		}
423 		print_link_stats(handle, fstable);
424 
425 	} else if (opt == FLOW_REPORT) {
426 		/* Display 1 flow */
427 		if (flowname != NULL) {
428 			dladm_flow_attr_t fattr;
429 			if (dladm_flow_info(handle, flowname, &fattr) !=
430 			    DLADM_STATUS_OK)
431 				return;
432 			(void) flow_kstats(&fattr, kcp);
433 		/* Display all flows on all links */
434 		} else if (linkid == DATALINK_ALL_LINKID) {
435 			(void) dladm_walk_datalink_id(link_flowstats, handle,
436 			    (void *)kcp, DATALINK_CLASS_ALL,
437 			    DATALINK_ANY_MEDIATYPE, DLADM_OPT_ACTIVE);
438 		/* Display all flows on a link */
439 		} else if (linkid != DATALINK_INVALID_LINKID) {
440 			(void) dladm_walk_flow(flow_kstats, handle, linkid, kcp,
441 			    B_FALSE);
442 		}
443 		print_flow_stats(handle, fstable);
444 
445 		/* Print totals */
446 		(void) attron(A_BOLD);
447 		dlt = (double)totalstats.snaptime / (double)NANOSEC;
448 		ikbs = totalstats.rbytes / dlt / 1024;
449 		okbs = totalstats.obytes / dlt / 1024;
450 		ipks = totalstats.ipackets / dlt;
451 		opks = totalstats.opackets / dlt;
452 		(void) printw("\n%-25.25s", "Totals");
453 		(void) printw("%9.2f %9.2f %9.2f %9.2f ",
454 		    ikbs, okbs, ipks, opks);
455 		(void) attroff(A_BOLD);
456 	}
457 
458 	if (redraw)
459 		(void) clearok(stdscr, 1);
460 
461 	if (refresh() == ERR)
462 		return;
463 
464 	if (redraw) {
465 		(void) clearok(stdscr, 0);
466 		redraw = 0;
467 	}
468 }
469 
470 /* Exported functions */
471 
472 /*
473  * Continuously display link or flow statstics using a libcurses
474  * based display.
475  */
476 
477 void
478 dladm_continuous(dladm_handle_t handle, datalink_id_t linkid,
479     const char *flowname, int interval, int opt)
480 {
481 	kstat_ctl_t *kcp;
482 
483 	if ((kcp = kstat_open()) == NULL) {
484 		warn("kstat open operation failed");
485 		return;
486 	}
487 
488 	curses_init();
489 
490 	for (;;) {
491 
492 		if (handle_break)
493 			break;
494 
495 		stat_report(handle, kcp, linkid, flowname, opt);
496 
497 		(void) sleep(max(1, interval));
498 	}
499 
500 	(void) curses_fin();
501 	(void) kstat_close(kcp);
502 }
503 
504 /*
505  * dladm_kstat_lookup() is a modified version of kstat_lookup which
506  * adds the class as a selector.
507  */
508 
509 kstat_t *
510 dladm_kstat_lookup(kstat_ctl_t *kcp, const char *module, int instance,
511     const char *name, const char *class)
512 {
513 	kstat_t *ksp = NULL;
514 
515 	for (ksp = kcp->kc_chain; ksp != NULL; ksp = ksp->ks_next) {
516 		if ((module == NULL || strcmp(ksp->ks_module, module) == 0) &&
517 		    (instance == -1 || ksp->ks_instance == instance) &&
518 		    (name == NULL || strcmp(ksp->ks_name, name) == 0) &&
519 		    (class == NULL || strcmp(ksp->ks_class, class) == 0))
520 			return (ksp);
521 	}
522 
523 	errno = ENOENT;
524 	return (NULL);
525 }
526 
527 /*
528  * dladm_get_stats() populates the supplied pktsum_t structure with
529  * the input and output  packet and byte kstats from the kstat_t
530  * found with dladm_kstat_lookup.
531  */
532 void
533 dladm_get_stats(kstat_ctl_t *kcp, kstat_t *ksp, pktsum_t *stats)
534 {
535 
536 	if (kstat_read(kcp, ksp, NULL) == -1)
537 		return;
538 
539 	stats->snaptime = gethrtime();
540 
541 	if (dladm_kstat_value(ksp, "ipackets64", KSTAT_DATA_UINT64,
542 	    &stats->ipackets) < 0) {
543 		if (dladm_kstat_value(ksp, "ipackets", KSTAT_DATA_UINT64,
544 		    &stats->ipackets) < 0)
545 			return;
546 	}
547 
548 	if (dladm_kstat_value(ksp, "opackets64", KSTAT_DATA_UINT64,
549 	    &stats->opackets) < 0) {
550 		if (dladm_kstat_value(ksp, "opackets", KSTAT_DATA_UINT64,
551 		    &stats->opackets) < 0)
552 			return;
553 	}
554 
555 	if (dladm_kstat_value(ksp, "rbytes64", KSTAT_DATA_UINT64,
556 	    &stats->rbytes) < 0) {
557 		if (dladm_kstat_value(ksp, "rbytes", KSTAT_DATA_UINT64,
558 		    &stats->rbytes) < 0)
559 			return;
560 	}
561 
562 	if (dladm_kstat_value(ksp, "obytes64", KSTAT_DATA_UINT64,
563 	    &stats->obytes) < 0) {
564 		if (dladm_kstat_value(ksp, "obytes", KSTAT_DATA_UINT64,
565 		    &stats->obytes) < 0)
566 			return;
567 	}
568 
569 	if (dladm_kstat_value(ksp, "ierrors", KSTAT_DATA_UINT32,
570 	    &stats->ierrors) < 0) {
571 		if (dladm_kstat_value(ksp, "ierrors", KSTAT_DATA_UINT64,
572 		    &stats->ierrors) < 0)
573 		return;
574 	}
575 
576 	if (dladm_kstat_value(ksp, "oerrors", KSTAT_DATA_UINT32,
577 	    &stats->oerrors) < 0) {
578 		if (dladm_kstat_value(ksp, "oerrors", KSTAT_DATA_UINT64,
579 		    &stats->oerrors) < 0)
580 			return;
581 	}
582 }
583 
584 int
585 dladm_kstat_value(kstat_t *ksp, const char *name, uint8_t type, void *buf)
586 {
587 	kstat_named_t	*knp;
588 
589 	if ((knp = kstat_data_lookup(ksp, (char *)name)) == NULL)
590 		return (-1);
591 
592 	if (knp->data_type != type)
593 		return (-1);
594 
595 	switch (type) {
596 	case KSTAT_DATA_UINT64:
597 		*(uint64_t *)buf = knp->value.ui64;
598 		break;
599 	case KSTAT_DATA_UINT32:
600 		*(uint32_t *)buf = knp->value.ui32;
601 		break;
602 	default:
603 		return (-1);
604 	}
605 
606 	return (0);
607 }
608 
609 dladm_status_t
610 dladm_get_single_mac_stat(dladm_handle_t handle, datalink_id_t linkid,
611     const char *name, uint8_t type, void *val)
612 {
613 	kstat_ctl_t	*kcp;
614 	char		module[DLPI_LINKNAME_MAX];
615 	uint_t		instance;
616 	char 		link[DLPI_LINKNAME_MAX];
617 	dladm_status_t	status;
618 	uint32_t	flags, media;
619 	kstat_t		*ksp;
620 	dladm_phys_attr_t dpap;
621 
622 	if ((kcp = kstat_open()) == NULL) {
623 		warn("kstat_open operation failed");
624 		return (-1);
625 	}
626 
627 	if ((status = dladm_datalink_id2info(handle, linkid, &flags, NULL,
628 	    &media, link, DLPI_LINKNAME_MAX)) != DLADM_STATUS_OK)
629 		return (status);
630 
631 	if (media != DL_ETHER)
632 		return (DLADM_STATUS_LINKINVAL);
633 
634 	status = dladm_phys_info(handle, linkid, &dpap, DLADM_OPT_PERSIST);
635 
636 	if (status != DLADM_STATUS_OK)
637 		return (status);
638 
639 	status = dladm_parselink(dpap.dp_dev, module, &instance);
640 
641 	if (status != DLADM_STATUS_OK)
642 		return (status);
643 
644 	/*
645 	 * The kstat query could fail if the underlying MAC
646 	 * driver was already detached.
647 	 */
648 	if ((ksp = kstat_lookup(kcp, module, instance, "mac")) == NULL &&
649 	    (ksp = kstat_lookup(kcp, module, instance, NULL)) == NULL)
650 		goto bail;
651 
652 	if (kstat_read(kcp, ksp, NULL) == -1)
653 		goto bail;
654 
655 	if (dladm_kstat_value(ksp, name, type, val) < 0)
656 		goto bail;
657 
658 	(void) kstat_close(kcp);
659 	return (DLADM_STATUS_OK);
660 
661 bail:
662 	(void) kstat_close(kcp);
663 	return (dladm_errno2status(errno));
664 }
665 
666 /* Compute sum of 2 pktsums (s1 = s2 + s3) */
667 void
668 dladm_stats_total(pktsum_t *s1, pktsum_t *s2, pktsum_t *s3)
669 {
670 	s1->rbytes    = s2->rbytes    + s3->rbytes;
671 	s1->ipackets  = s2->ipackets  + s3->ipackets;
672 	s1->ierrors   = s2->ierrors   + s3->ierrors;
673 	s1->obytes    = s2->obytes    + s3->obytes;
674 	s1->opackets  = s2->opackets  + s3->opackets;
675 	s1->oerrors   = s2->oerrors   + s3->oerrors;
676 	s1->snaptime  = s2->snaptime;
677 }
678 
679 /* Compute differences between 2 pktsums (s1 = s2 - s3) */
680 void
681 dladm_stats_diff(pktsum_t *s1, pktsum_t *s2, pktsum_t *s3)
682 {
683 	s1->rbytes    = s2->rbytes    - s3->rbytes;
684 	s1->ipackets  = s2->ipackets  - s3->ipackets;
685 	s1->ierrors   = s2->ierrors   - s3->ierrors;
686 	s1->obytes    = s2->obytes    - s3->obytes;
687 	s1->opackets  = s2->opackets  - s3->opackets;
688 	s1->oerrors   = s2->oerrors   - s3->oerrors;
689 	s1->snaptime  = s2->snaptime  - s3->snaptime;
690 }
691