-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathexplain-pause-mode.el
3699 lines (3263 loc) · 152 KB
/
explain-pause-mode.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; explain-pause-mode.el --- explain emacs pauses -*- lexical-binding: t; emacs-lisp-docstring-fill-column: 80; fill-column: 80; -*-
;; Copyright (C) 2020 Lin Xu
;; Author: Lin Xu <[email protected]>
;; Version: 0.1
;; Created: May 18, 2020
;; Keywords: performance speed config
;; URL: https://github.com/lastquestion/explain-pause-mode
;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Commentary:
;; `explain-pause-mode' is a minor mode that measures how long Emacs commands
;; take to run. `explain-pause-top' is like 'top', but for Emacs.
;;
;; When a command consistently takes a long time to run, `explain-pause-mode'
;; alerts the user and records the next time the command is slow. These "slow
;; events" can be viewed in `explain-pause-top' and investigated immediately,
;; or summarized to be sent to developers.
;; Please see README.md for commentary, documentation, etc. in the repository
;; above.
;;; Code:
(defconst explain-pause-version 0.1
"Explain-pause version")
(require 'seq)
(require 'profiler)
(require 'subr-x)
(require 'nadvice)
(require 'cl-macs)
;; don't type check. note this only applies when (cl--compiling-file) returns t
;; - e.g. when it's bytecompiled.
(cl-declaim (optimize (safety 0) (speed 3)))
;; customizable behavior
(defgroup explain-pause nil
"Explain pauses in Emacs"
:prefix "explain-pause-"
:group 'development)
(defgroup explain-pause-logging nil
"Explain pause logging"
:prefix "explain-pause-log-"
:group 'explain-pause)
(defgroup explain-pause-alerting nil
"Explain pause alerting"
:prefix "explain-pause-alert-"
:group 'explain-pause)
(defgroup explain-pause-profiling nil
"Explain pause profiling"
:prefix "explain-pause-profile-"
:group 'explain-pause)
(defgroup explain-pause-top nil
"Explain pause top major mode"
:prefix "explain-pause-top-"
:group 'explain-pause)
;; main behaviors
(defcustom explain-pause-slow-too-long-ms 40
"How long must some activity take before explain-pause considers it slow, in ms?"
:type 'integer
:group 'explain-pause)
(defcustom explain-pause-top-auto-refresh-interval 2
"How often `explain-pause-top' mode buffers refresh themselves by default,
in seconds. This can be a fraction of a second. If this is nil, they
do not automatically refresh. You can control this on a per buffer basis
by calling `explain-pause-top-auto-refresh'."
:type '(choice (number :tag "Interval (seconds)")
(const :tag "Never" nil))
:group 'explain-pause-top)
(defcustom explain-pause-top-click-profile-action #'switch-to-buffer-other-window
"The function that is called when the user clicks on the profile button in
`explain-pause-top' buffers. The function is passed PROFILE-BUFFER, the buffer
which holds the generated profile output. You can customize this to change the
behavior if you wish. The default is to view the buffer using
`switch-to-buffer-other-window'."
:type 'function
:group 'explain-pause-top)
;; profiling behaviors
(defcustom explain-pause-profile-slow-threshold 3
"Explain-pause will profile a slow activity once it has executed slowly this
many times."
:type 'integer
:group 'explain-pause-profiling)
(defcustom explain-pause-profile-cpu-sampling-interval 200000
"The CPU sampling interval when the profiler is activated in microseconds.
The default value is 2ms."
:type 'integer
:group 'explain-pause-profiling)
(defcustom explain-pause-profile-saved-profiles 5
"The number of CPU profiles to save for each command, after which the oldest
is removed. Changes to this number apply to new commands only. If you wish,
you may run `explain-pause-profile-clear' to clear all profiles, though
this will not clear statistics from individual `explain-top-mode' buffers."
:type 'integer
:group 'explain-pause-profiling)
(defcustom explain-pause-profile-enabled t
"Should explain-pause profile slow activities at all?"
:type 'boolean
:group 'explain-pause-profiling)
;; public hooks
(defvar explain-pause-measured-command-hook nil
"Functions(s) to call after a command has been measured. The functions are
called with an explain-pause-command-record argument.
These commands must be fast, because this hook is executed on every command,
not just slow commands. You cannot give up execution in these commands in
any way, e.g. do not call any family of functions that `sit-for', `read-key',
etc. etc.")
;; custom faces
(defface explain-pause-top-slow
'((t (:foreground "red")))
"The face used to highlight the slow count column when a command is slow
(e.g. > 1 hit)."
:group 'explain-pause-top)
(defface explain-pause-top-profile-heading
'((t (:inherit warning)))
"The face used to highlight the profile heading for commands which have
profiles available to view."
:group 'explain-pause-top)
(defface explain-pause-top-slow-heading
'((t (:inherit warning)))
"The face used to highlight the slow times heading for commands which have
slow times."
:group 'explain-pause-top)
(defface explain-pause-top-changed
'((t (:inherit bold)))
"The face used to indicate that a value changed since the last refresh of the
buffer."
:group 'explain-pause-top)
(defface explain-pause-top-active-column-header
'((t (:inherit header-line-highlight)))
"The face used to indicate the currently sorted column in the header line."
:group 'explain-pause-top)
(defcustom explain-pause-alert-normal-interval 15
"What is the minimum amount of time, in minutes, between alerts when
`explain-pause-alert-style' is normal? You can put a fractional value if you
wish."
:type 'number
:group 'explain-pause-alerting)
(defcustom explain-pause-alert-normal-minimum-count 5
"How many slow events must occur before `explain-pause' alerts you when
`explain-pause-alert-style' is normal?"
:type 'integer
:group 'explain-pause-alerting)
(defvar explain-pause-mode)
;; time lists are too expensive to create every single call
;; convert to a integer of ms.
(defsubst explain-pause--as-ms-exact (time)
"Returns the TIME object in exact ms, ignoring picoseconds."
(+ (* (+ (* (nth 0 time) 65536) (nth 1 time)) 1000)
(/ (nth 2 time) 1000)))
;; TODO perhaps this should also display minor modes? probably. minor modes can be interact
;; weirdly and become slow.
;; TODO these aren't used right now
(defun explain--buffer-as-string ()
"Return a human readable string about the buffer (name + major mode)."
(format "%s (%s)"
(buffer-name)
major-mode))
(defun explain--buffers-as-string (buffers)
"Return a human readable string for all BUFFERS given."
(mapconcat (lambda (buffer)
(with-current-buffer buffer
(explain--buffer-as-string)))
buffers ", "))
(defun explain-pause--command-as-string (cmd)
"Generate a human readable string for a command CMD.
Normally this is a symbol, when we are in a command loop, but in timers, process
filters, etc. this might be a lambda or a bytecompiled lambda. In those cases,
also handle if the forms are wrapped by closure. For bytecompiled code, use the
references as the best information available. For lambdas and closures, hope
that the argument names are clarifying. Also subrp is allowed, as we can
generate native frames. We also allow strings for things that need special
representatinos. Note that in elisp, symbols may have %! So e.g. this function
may generate strings with format specifiers in them."
(cond
((stringp cmd) cmd)
((symbolp cmd) (symbol-name cmd))
;; TODO is there nicer ways to get this?
((subrp cmd) (prin1-to-string cmd t))
((byte-code-function-p cmd)
;; "The vector of Lisp objects referenced by the byte code. These include
;; symbols used as function names and variable names."
;; list only symbol references:
(format "<bytecode> (references: %s)"
(seq-filter #'symbolp (aref cmd 2))))
((not (listp cmd))
;; something weird. This should not happen.
(format "Unknown (please file a bug) %s" cmd))
;; closure. hypothetically, this is defined as a implementation detail,
;; but we'll read it anyway...
((eq (car cmd) 'closure)
(format "<closure> (arg-list: %s)" (nth 2 cmd)))
;; lambda. directly read the arg-list:
((eq (car cmd) 'lambda)
(format "<lambda> (arg-list: %s)" (nth 1 cmd)))
(t
(format "Unknown (please file a bug) %s" cmd))))
;; TODO not used right now...
(defun explain-pause--command-set-as-string (command-set)
"Format a COMMAND-SET as a human readable string.
A command set is a list of commands that represent the context that lead to the
blocking execution (or we think so, anyway)."
(mapconcat
#'explain-pause--command-as-string
command-set ", "))
;; the record of an command that we measured
;; theorywise, we are constructing a tree of records, all rooted at "emacs command
;; loop". Idealistically, we could maintain this tree and calculate the timings
;; by subtracting child times from our own. But because elisp actually executes
;; only one thing at a time, structure the graph as a stack and pause tracking
;; as we enter / exit by push/popping - we're traversing the graph as DFS
;; as we execute.
(cl-defstruct explain-pause-command-record
;; the command this tracked
command
;; was this a native frame
native
;; the parent
parent
;; timing
;; the number of ms spent so far.
(executing-time 0)
;; a TIME object as snap
entry-snap
;; was this too slow
too-slow
;; profiling:
;; was profiling was started FOR this command
is-profiled
;; was profiling started when this command started
under-profile
;; the profile if it was
profile
;; depth of the callstack so far
depth)
(defconst explain-pause-root-command-loop
(make-explain-pause-command-record
:command 'root-emacs
:depth 0)
"All command records that `explain-pause' tracks ultimately are rooted to this
command entry, which represents the top level command loop that begins in
`keyboard.c' when called from the initial `recursive_edit' from `emacs.c'.")
;; profiling and slow statistics functions
;; TODO :equal list command
(defvar explain-pause-profile--profile-statistics (make-hash-table)
"A hash map of the slow commands and their statistics.
This data is always gathered and stored when `explain-pause-mode' is
active. When `explain-pause-profile-enabled' is true, profiling logs are also
stored. Each entry is a VECTOR of values. In an effort to optimize memory
allocations, store the slow counts inline with the rest of the object
instead of using a cl-struct with a field of a vector.")
(defconst explain-pause-profile--statistic-defaults
[0 ;; profile-counter
nil ;; should-profile-next
0 ;; profile-attempts
nil ;; list-of-profiles
0 ;; slow-count
nil];; slow-ms-idx
"A constant vector of defaults used when upset to the statistics hashmap is
cnot required.")
(defconst explain-pause-profile--statistic-slow-count-offset
6
"The offset into the vector of statistic where the first slow ms is found.")
(defsubst explain-pause-profile--statistic-slow-length (statistic)
"Return the number of slow counts available in this STATISTIC"
(- (length statistic)
explain-pause-profile--statistic-slow-count-offset))
(defsubst explain-pause-profile--statistic-profile-p (record)
"Whether the command represented by RECORD should be profiled. Does not create
a new entry if the command has not been seen; in that case, returns nil."
(aref (gethash (explain-pause-command-record-command record)
explain-pause-profile--profile-statistics
explain-pause-profile--statistic-defaults)
1))
(defsubst explain-pause-profile--statistic-profiles (record)
"Get the profiles for a command represented by RECORD."
(aref (gethash (explain-pause-command-record-command record)
explain-pause-profile--profile-statistics
explain-pause-profile--statistic-defaults)
3))
(defsubst explain-pause-profile--statistic-profile-attempts (record)
"Get the attempts to profile for a command represented by RECORD."
(aref (gethash (explain-pause-command-record-command record)
explain-pause-profile--profile-statistics
explain-pause-profile--statistic-defaults)
2))
(defsubst explain-pause-profile--statistic-slow-index (record)
"Get the current index of the circular list of slow times in RECORD."
(aref (gethash (explain-pause-command-record-command record)
explain-pause-profile--profile-statistics
explain-pause-profile--statistic-defaults)
5))
(defsubst explain-pause-profile--statistic-slow-count (record)
"Get the current index of the circular list of slow times in RECORD."
(aref (gethash (explain-pause-command-record-command record)
explain-pause-profile--profile-statistics
explain-pause-profile--statistic-defaults)
4))
(defun explain-pause-profile-clear ()
"Clear the profiling data. Note that this does not clear profiles already visible
in any `explain-pause-top' buffers."
(interactive)
(clrhash explain-pause-profile--profile-statistics))
(defun explain-pause-profiles-ignore-command (_command-set)
"Ignore this command-set from ever being profiled."
;;TODO (interactive)
t)
(defmacro explain-pause-profile--profile-get-statistic (record)
;; define this as a macro because a defsubst cannot inline before the owning
;; let has finished (e.g. this can't be inside the next closure and be used
;; in `explain-pause-profile--profile-measured-command'
`(progn
(setq command (explain-pause-command-record-command ,record))
(setq statistic (gethash command explain-pause-profile--profile-statistics nil))
(unless statistic
(setq statistic (make-vector
(+ explain-pause-profile--statistic-slow-count-offset
explain-pause-profile-saved-profiles)
nil))
(cl-loop
for new-stat across-ref statistic
for default-stat across explain-pause-profile--statistic-defaults
do
(setf new-stat default-stat))
(puthash command statistic explain-pause-profile--profile-statistics))))
(eval-and-compile
;; for the mainline case, no profiles are stored but values are incremented
;; store these outside in a closure, so we don't need to create lets every call.
(let ((profile nil)
(statistic nil)
(command nil)
(slow-index nil))
(defun explain-pause-profile--profile-measured-command (record)
"Record the statistics for this command.
Always store the slowness. If profiling is on, store the profiling counts.
Store the profile if it was profiled."
(unless (explain-pause-command-record-native record)
(cond
;; did we try to profile but it was too fast? if this happens more
;; then threshold times, reset the counter back to 0
((and (explain-pause-command-record-is-profiled record)
(not (explain-pause-command-record-too-slow record)))
(explain-pause-profile--profile-get-statistic record)
;; reuse profile var for attempt counter
(setq profile (aref statistic 2))
(if (< profile explain-pause-profile-saved-profiles)
(setf (aref statistic 2) (1+ profile))
;; give up TODO force?
(setf (aref statistic 0) 0)
(setf (aref statistic 1) nil)
(setf (aref statistic 2) 0)))
((explain-pause-command-record-too-slow record)
;; otherwise, if we're too slow...
(explain-pause-profile--profile-get-statistic record)
(setq profile (explain-pause-command-record-profile record))
;; increment the slow count
(setf (aref statistic 4) (1+ (aref statistic 4)))
;; save the ms into the circular list
(setq slow-index (or (aref statistic 5) 0))
(setf (aref statistic (+ slow-index
explain-pause-profile--statistic-slow-count-offset))
(explain-pause-command-record-executing-time record))
;; increment slow-ms-index to the next place
(setf (aref statistic 5)
(% (1+ slow-index)
;; don't use `explain-pause-profile-saved-profiles' because the value
;; might have changed
(explain-pause-profile--statistic-slow-length statistic)))
(cond
;; add the profile if it exists.
;; we assume that profiles happen relatively rarely, so it's ok to use
;; a list so that 'eq comparisons work against head:
(profile
(let ((head (aref statistic 3))
(new-entry (vector
(explain-pause-command-record-executing-time record)
profile)))
(setf (aref statistic 3)
(if (< (length head)
explain-pause-profile-saved-profiles)
(cons new-entry head)
;; need to make a duplicate list
(cons new-entry
(seq-take head
(- explain-pause-profile-saved-profiles 1))))))
;; reset for next time
(setf (aref statistic 0) 0)
(setf (aref statistic 1) nil))
(t
;; reuse profile var for the counter here
(setq profile (aref statistic 0))
(when (>= profile 0) ;; only increment for "non-special" counts
(setq profile (1+ profile))
(setf (aref statistic 0) profile)
(setf (aref statistic 1)
(>= profile explain-pause-profile-slow-threshold)))))))))))
;; table functions
;; I tried to use `tabulated-list' as well as `ewoc' but I decided to implement
;; something myself. This list/table implements some ideas that are from react/JS
;; like philosophies around optimizing drawing...
;; part of it is already abstracted out into something close to reusable, but
;; other parts are not yet.
(cl-defstruct explain-pause-top--table
;; the list of entries to display, in sorted order
;; (item prev-display-ptr)
;; to simplify list manipulation code, always have a head
(entries (list nil))
;; the display entries bookkeeping; a list of explain-pause-top--table-display-entry
(display-entries (list nil))
;; the sort. it must be set before any inserts or updates.
(sorter nil)
;; the current width
(width 0)
;; whether on next paint, we need to resize
(needs-resize t)
;; the number of COLUMNS, for which each must have a HEADER.
column-count
;; the number of fields. Fields after COLUMN-COUNT are printed as full lines.
field-count
;; A VECTOR of widths of every column
column-widths
;; A VECTOR of widths of every header
header-widths
;; A VECTOR of header titles. must be set before we attempt to draw.
(header-titles nil)
;; whether the header is dirty
header-dirty
;; the full line format string
display-full-line-format
;; A VECTOR of format strings for every column
display-column-formats
;; A VECTOR of offsets of every column
display-column-offsets
;; the index into the buffer vector representing which buffer we are rendering into
buffer-index
;; the previous buffer index
prev-buffer-index
;; the width of the buffer (1 + fields + columns)
buffer-width
;; A scratch diff VECTOR so we don't have to reallocate every draw.
current-diffs
;; A scratch diff VECTOR of requested widths for COLUMNS so we don't have to reallocate
requested-widths)
(cl-defstruct explain-pause-top--table-display-entry
;; info about the entry in the emacs buffer
begin-mark
;; the total display length of this item. begin-market + total-length => '\n'
total-length
;; each entry holds a VECTOR of data, one set for each BUFFER
;; (not emacs buffers, double buffering)
;; [item-ptr string-vals (0-FIELD) string-lengths (O-COLUMN)]
buffer
;; A VECTOR of the dirtiness of FIELDS (nil or t)
dirty-fields)
(defun explain-pause-top--table-set-sorter (table new-sort &optional fast-flip)
"Change the sort function. Does not re-render.
If fast-flip is set, simply reverse the entries. The new sort function
must actually implement the reversed order, it (and sort) are just not
called."
;; note that we do not need to copy or move around prev-display-ptr as
;; no item is added or removed.
;; skip over the head
(let* ((entry-ptrs (cdr (explain-pause-top--table-entries table)))
(sorted-ptrs (if fast-flip
(reverse entry-ptrs)
(sort entry-ptrs
;; the sort we do is flipped
(lambda (lhs rhs)
(not (funcall new-sort
(car lhs)
(car rhs))))))))
(setf (explain-pause-top--table-entries table)
(cons nil sorted-ptrs))
(setf (explain-pause-top--table-sorter table)
new-sort)))
(defun explain-pause-top--table-find-and-insert (table item)
"insert ITEM into the entries, sorted by the current sort function. If the
item is found by the time insertion happens, return the prev item (whose cdr
points to the item). If it is not found, return the newly added item.
Comparison of items is by `eq'. If the new item would have been inserted at the
exact same place as the existing item, no insertion occurs, and nil is
returned."
(let* ((ptr-entry nil) ;; don't allocate it unless we absolutely need it
(display-order-prev (explain-pause-top--table-entries table))
(display-order-ptr (cdr display-order-prev))
(sort-function (explain-pause-top--table-sorter table))
(saved-dup-item-entry nil)
(saved-prev-item nil))
;; insert and search the list at the same time
(catch 'inserted
(while display-order-ptr
(let ((compare-item (caar display-order-ptr)))
;; it is very common we only update a value without changing
;; the order of the list. check for that case here, so we
;; don't create objects just to throw them away in the update
;; function
(if (eq compare-item item)
;; exactly equal; we've found the previous entry
;; would we have inserted the new item here?
(let ((next-item (cdr display-order-ptr)))
;; if there is no next, then we are at the end anyway,
;; and certainly we would replace ourselves
(when (or (not next-item)
(funcall sort-function (caar next-item) item))
;; yes: get outta here.
(throw 'inserted nil))
;; otherwise, record where it is, and skip past it
(setq saved-prev-item (cdar display-order-ptr))
(setq saved-dup-item-entry display-order-prev))
;; not equal - actual compare:
(when (funcall sort-function compare-item item)
;; we can insert.
;; did we find the item already? if so, copy the prev-ptr, as well
(setq ptr-entry (cons
(cons item saved-prev-item)
display-order-ptr))
(setcdr display-order-prev ptr-entry)
;; finish early
(throw 'inserted nil)))
(setq display-order-prev display-order-ptr)
(setq display-order-ptr (cdr display-order-ptr))))
;; at the end, and we didn't insert
(setq ptr-entry (cons
(cons item saved-prev-item)
nil))
(setcdr display-order-prev ptr-entry))
(or saved-dup-item-entry
ptr-entry)))
(defun explain-pause-top--table-insert (table item)
"Insert an item into the entries. It will be inserted at the correct place
with the current sort function. It is expected that an item is ever only
inserted once."
(explain-pause-top--table-find-and-insert table item))
(defun explain-pause-top--table-update (table item)
"Update an item in the entries. It will be moved to the correct place
with the current sort function. It is more efficient to call
`explain-pause-top--table-insert' if you know the entry is not in the
table yet, but this will succeed even if this is not true."
(let* ((prev
(explain-pause-top--table-find-and-insert table item))
(ptr (cdr prev)))
;; if prev is nil, we don't need to do anything at all;
;; it means that the place in the list did not change.
(when prev
;; otherwise, we have to clean up the old entry:
(when (eq (caar prev) item)
;; if the returned item is the item we just inserted, it means
;; that insert did not find the old item. keep on searching for it:
(let ((new-item prev))
(catch 'found
(while ptr
(when (eq (caar ptr) item)
;; prev now points to the old item to delete.
;; copy the prev-ptr to the new-item
(setcdr (car new-item) (cdar ptr))
(throw 'found nil))
(setq prev ptr)
(setq ptr (cdr ptr))))))
;; ok, splice the old one out
(setcdr prev (cdr ptr)))))
(defun explain-pause-top--table-clear (table)
"Clear all items in the table"
;; TODO delete all the other entries
(setf (explain-pause-top--table-entries table) (list nil)))
(defun explain-pause-top--table-initialize
(table headers field-count)
"Initialize headers, field infformation, and scratch buffers for TABLE. Must
be run in the buffer it is expected to draw in, because it also initializes
header widths."
(let* ((column-count (length headers))
(buffer-width (+ 1 field-count column-count)))
;; field and column sizes
(setf (explain-pause-top--table-column-count table) column-count)
(setf (explain-pause-top--table-field-count table) field-count)
(setf (explain-pause-top--table-buffer-width table) buffer-width)
;; scratch objects
(setf (explain-pause-top--table-requested-widths table)
(make-vector column-count nil))
(setf (explain-pause-top--table-current-diffs table)
(make-vector field-count nil))
(setf (explain-pause-top--table-buffer-index table) 0)
(setf (explain-pause-top--table-prev-buffer-index table) buffer-width)
;; header info
(setf (explain-pause-top--table-header-dirty table) t)
(setf (explain-pause-top--table-header-titles table) headers)
(setf (explain-pause-top--table-header-widths table)
(cl-map 'vector #'string-width headers))))
(defun explain-pause-top--table-set-header (table idx header)
"Set one header to a new value. Must be run in the buffer it is expected to
draw in, as it needs to calculate the width."
(setf (aref (explain-pause-top--table-header-titles table) idx) header)
(setf (aref (explain-pause-top--table-header-widths table) idx)
(string-width header))
(setf (explain-pause-top--table-header-dirty table) t))
(defun explain-pause-top--table-generate-offsets (fill-width widths)
"Return a vector of offsets for FILL-WIDTH and then all the columns in list WIDTHS.
Columns in WIDTHS get one character padding in between each."
(cl-loop
for width in widths
with accum = fill-width
collect accum into offsets
do (setq accum (+ accum 1 width))
finally return (apply 'vector 0 offsets)))
(defun explain-pause-top--table-resize-columns (table fixed-widths)
"Resize the columns within a table to new fixed widths given. Does NOT need to
be run within the current buffer, as it never runs `string-width'."
(let*
((width (explain-pause-top--table-width table))
(total-fixed (+ (apply #'+ fixed-widths)
;; one space between every fixed column
(- (length fixed-widths) 1)))
;; the beginning of the fixed base, aka the width of the fill column
(fill-width (- width total-fixed))
(final-widths
(apply 'vector fill-width fixed-widths))
(column-offsets
(explain-pause-top--table-generate-offsets
fill-width fixed-widths))
;; now generate the fill format string; it's left justified:
(fill-format-string
(format "%%-%d.%ds" fill-width fill-width))
;; and the fixed format strings:
(fixed-format-string-list
(mapcar
(lambda (width)
;; ask for the column to be padded to be right
;; justified, but also to limit the total characters
;; to the same width.
(format "%%%d.%ds" width width))
fixed-widths))
;; now generate the vector of format strings for every column
(format-string-list
(apply 'vector fill-format-string fixed-format-string-list))
;; now generate the full format line for use when inserting a full row
;; (and header line)
(full-format-string
(concat fill-format-string
(mapconcat #'identity fixed-format-string-list " "))))
(setf (explain-pause-top--table-display-full-line-format table)
full-format-string)
(setf (explain-pause-top--table-column-widths table)
final-widths)
(setf (explain-pause-top--table-display-column-offsets table)
column-offsets)
(setf (explain-pause-top--table-display-column-formats table)
format-string-list)
(setf (explain-pause-top--table-header-dirty table) t)))
(defun explain-pause-top--table-resize-width (table width)
"Resize the table by updating the width and setting the dirty width
flag. Does not draw, nor recalculate any widths."
(setf (explain-pause-top--table-width table) width)
(setf (explain-pause-top--table-needs-resize table) t))
(defconst explain-pause-top--header-left-alignment
(propertize " " 'display (cons 'space (list :align-to 0)))
;; this is how we deal with left margins fringes and so on, as those are
;; pixel sized, so we can't print spaces.
"The display property to left align the header to the beginning of the body")
(defun explain-pause-top--generate-header-line
(header header-length window-scroll window-width)
"Generate a truncated header line. The header scrolls with the text, and
adds '$' when there is more header either front or end."
;; TODO this should probably use the actual continuation glyph?
;; TODO these really need to be test cases:
;; text |-------|
;; window |--------|
;; window |-------|
;; window |--------------|
;; window |---|
;; window |-------|
;; window -|
;; first, calculate the window we "should" watch over:
(let* ((start window-scroll)
(end (+ start window-width))
;; next, if the window is outside the bounds, adjust the bounds to match
(bounded-start (min (max 0 start) header-length))
(bounded-end (min (max 0 end) header-length))
;; next, calculate if we need dots:
(head-dots (> bounded-start 0))
(end-dots (< bounded-end header-length))
;; the dot strings
(head-dot-str (when head-dots "$"))
(end-dot-str (when end-dots "$"))
;; the head padding, which only applies if we've negatively scrolled
(head-padding (when (< start 0)
(make-string (- start) ? ))))
(when head-dots
(if (< bounded-start header-length)
;; we need dots at the front and we can move forward.
(setq bounded-start (1+ bounded-start))
;; if the move forward would have moved the bounded start
;; beyond the string, set start and end to 0 and clear the
;; end dot str:
(setq bounded-start 0)
(setq bounded-end 0)
(setq end-dot-str nil)))
(when end-dots
(let ((new-end (- bounded-end 1)))
(if (>= new-end bounded-start)
;; if we can go backwards, all's ok
(setq bounded-end new-end)
;; if not, this means bounded-start == bounded-end,
;; and we don't have space to insert a $. do nothing
(setq end-dot-str nil))))
(concat explain-pause-top--header-left-alignment
head-padding
head-dot-str
(substring header bounded-start bounded-end)
end-dot-str)))
(defun explain-pause-top--concat-to-width (strings width separator)
"Concat STRINGS together with a space until WIDTH is reached, and then insert
SEPARATOR. The width of separator counts towards the next group, not the prior
one. At least one item will always fit in each group, even if the item is wider
then WIDTH."
(let* ((group-length 0)
(reversed-final nil)
(item-ptr strings))
(while item-ptr
(let* ((item (car item-ptr))
(this-length (length item))
;; +1 for the space if it's not first item
(try-length (+ group-length this-length
(if (eq group-length 0) 0 1))))
(if (<= try-length width)
(progn
(setq reversed-final
(cons item
(if (eq group-length 0)
;; first item
reversed-final
(cons " " reversed-final))))
(setq group-length try-length)
(setq item-ptr (cdr item-ptr)))
;; break
(if (> group-length 0)
;; at least one item has been pushed;
(setq reversed-final
(cons separator reversed-final))
;; only this item; push anyway
(setq reversed-final
(cons separator
(cons item reversed-final)))
(setq item-ptr (cdr item-ptr)))
;; clear group length for next round
(setq group-length 0))))
(apply 'concat (reverse reversed-final))))
(defun explain-pause-top--split-at-space (string max-lengths)
"Split a string at max-lengths or less, if possible, at a space boundary. If
not possible, split at (car MAX-LENGTH) - 1 and add a \\ continuation. Use up
MAX-LENGTHS until only one remains, which becomes the final max-length for
the rest of the lines."
(let* ((splits (split-string string " +" t))
(current-line-length 0)
(current-line nil)
(results nil))
(while splits
(let* ((this-split (car splits))
(this-length (length this-split))
(try (+ current-line-length this-length (length current-line)))
(this-max-length (car max-lengths)))
(if (<= try this-max-length)
;; fits
(progn
(push this-split current-line)
(setq splits (cdr splits))
(setq current-line-length (+ current-line-length this-length)))
;; doesn't fit
(if current-line
;; some stuff filled, start a new line and try again
(push current-line results)
;; cut the string up
(let* ((split-point (- this-max-length 1))
(first-half (substring this-split 0 split-point))
(second-half (substring this-split split-point)))
(push (list (concat first-half "\\")) results)
(setq splits (cons second-half (cdr splits)))))
;; clear the line
(setq current-line nil)
(setq current-line-length 0)
;; next max-length
(when (cdr max-lengths)
(setq max-lengths (cdr max-lengths))))))
(when current-line
(push current-line results))
(cl-loop
for line in (reverse results)
collect (string-join (reverse line) " "))))
(defsubst explain-pause-top--table-item-command-overflow
(command-column-width full-width command-string)
"Return nil or the (first, rest) strings for COMMAND-STRING."
;; TODO this really should be renamed and moved to the command entry
;; area
(if (< (length command-string)
command-column-width)
;; it fits
nil
;; ok, truncate and split:
(let ((lines
(explain-pause-top--split-at-space
command-string
(list command-column-width
(- full-width 2))))
(indent-newline "\n "))
(cons (car lines)
(concat indent-newline
(string-join (cdr lines) indent-newline))))))
(defsubst explain-pause-top--table-prepare-draw
(entry new-data buffer-index prev-buffer-index
column-count field-count requested-widths field-diffs)
"Prepare to draw ENTRY by setting the item to draw to NEW-DATA, then
generating the converted strings from the values. Store the strings and their
lengths into the buffer at BUFFER-INDEX, using the old values at
PREV-BUFFER-INDEX if useful. Finally, update REQUESTED-WIDTHS and dirty-fields
within the item with their dirtiness. FIELD-DIFFS is a temporary vector used to
hold the difference of fields."
(let* ((to-draw-item (car new-data))
(prev-draw-entry (cdr new-data))
(dirty-fields (explain-pause-top--table-display-entry-dirty-fields entry))
(buffer (explain-pause-top--table-display-entry-buffer entry))
(prev-entry-buffer
(when prev-draw-entry
(explain-pause-top--table-display-entry-buffer prev-draw-entry))))
;; given the inputs, ask the entry to fill in the new state with new strings
(setf (aref buffer buffer-index)
(explain-pause-top--command-entry-compare
(aref buffer buffer-index)
;; the new thing we want to draw
to-draw-item
;; the previous item drawn here
(aref buffer prev-buffer-index)
;; the previous drawn of item
(when prev-entry-buffer
(aref prev-entry-buffer prev-buffer-index))
field-diffs))
;; update the item-ptr's prev-ptr to point to entry. we've saved the
;; actual prev-ptr already.
(setcdr new-data entry)
;; current item-ptr is now filled with the new values, and field-diffs
;; holds the new strings, or where to copy.
(cl-loop
for field-index from 0
for buffer-field from (1+ buffer-index)
for prev-buffer-field from 1
for field-diff across field-diffs
for dirty-field across-ref dirty-fields
with copy-buffer = nil
with is-field = nil
with field-width = 0
do
(setq is-field (< field-index column-count))
(cond
((eq field-diff 'explain-pause-top--table-prev-item)
(setq copy-buffer buffer)
(setq dirty-field nil))
((eq field-diff 'explain-pause-top--table-prev-drawn)
(setq copy-buffer prev-entry-buffer)
(setq dirty-field t))
(t
(setq copy-buffer nil)
(setq dirty-field t)))
(setf (aref buffer buffer-field)
(if copy-buffer
(aref copy-buffer (+ prev-buffer-index prev-buffer-field))
field-diff))
(when is-field
;; update stored length
(setq field-width