1;;; elp.el --- Emacs Lisp Profiler 2 3;; Copyright (C) 1994, 1995, 1997, 1998, 2001, 2002, 2003, 2004, 4;; 2005, 2006, 2007 Free Software Foundation, Inc. 5 6;; Author: Barry A. Warsaw 7;; Maintainer: FSF 8;; Created: 26-Feb-1994 9;; Keywords: debugging lisp tools 10 11;; This file is part of GNU Emacs. 12 13;; GNU Emacs is free software; you can redistribute it and/or modify 14;; it under the terms of the GNU General Public License as published by 15;; the Free Software Foundation; either version 2, or (at your option) 16;; any later version. 17 18;; GNU Emacs is distributed in the hope that it will be useful, 19;; but WITHOUT ANY WARRANTY; without even the implied warranty of 20;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21;; GNU General Public License for more details. 22 23;; You should have received a copy of the GNU General Public License 24;; along with GNU Emacs; see the file COPYING. If not, write to the 25;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 26;; Boston, MA 02110-1301, USA. 27 28;;; Commentary: 29;; 30;; If you want to profile a bunch of functions, set elp-function-list 31;; to the list of symbols, then do a M-x elp-instrument-list. This 32;; hacks those functions so that profiling information is recorded 33;; whenever they are called. To print out the current results, use 34;; M-x elp-results. If you want output to go to standard-output 35;; instead of a separate buffer, setq elp-use-standard-output to 36;; non-nil. With elp-reset-after-results set to non-nil, profiling 37;; information will be reset whenever the results are displayed. You 38;; can also reset all profiling info at any time with M-x 39;; elp-reset-all. 40;; 41;; You can also instrument all functions in a package, provided that 42;; the package follows the GNU coding standard of a common textual 43;; prefix. Use M-x elp-instrument-package for this. 44;; 45;; If you want to sort the results, set elp-sort-by-function to some 46;; predicate function. The three most obvious choices are predefined: 47;; elp-sort-by-call-count, elp-sort-by-average-time, and 48;; elp-sort-by-total-time. Also, you can prune from the output, all 49;; functions that have been called fewer than a given number of times 50;; by setting elp-report-limit. 51;; 52;; Elp can instrument byte-compiled functions just as easily as 53;; interpreted functions, but it cannot instrument macros. However, 54;; when you redefine a function (e.g. with eval-defun), you'll need to 55;; re-instrument it with M-x elp-instrument-function. This will also 56;; reset profiling information for that function. Elp can handle 57;; interactive functions (i.e. commands), but of course any time spent 58;; idling for user prompts will show up in the timing results. 59;; 60;; You can also designate a `master' function. Profiling times will 61;; be gathered for instrumented functions only during execution of 62;; this master function. Thus, if you have some defuns like: 63;; 64;; (defun foo () (do-something-time-intensive)) 65;; (defun bar () (foo)) 66;; (defun baz () (bar) (foo)) 67;; 68;; and you want to find out the amount of time spent in bar and foo, 69;; but only during execution of bar, make bar the master. The call of 70;; foo from baz will not add to foo's total timing sums. Use M-x 71;; elp-set-master and M-x elp-unset-master to utilize this feature. 72;; Only one master function can be set at a time. 73 74;; You can restore any function's original function definition with 75;; elp-restore-function. The other instrument, restore, and reset 76;; functions are provided for symmetry. 77 78;; Here is a list of variable you can use to customize elp: 79;; elp-function-list 80;; elp-reset-after-results 81;; elp-sort-by-function 82;; elp-report-limit 83;; 84;; Here is a list of the interactive commands you can use: 85;; elp-instrument-function 86;; elp-restore-function 87;; elp-instrument-list 88;; elp-restore-list 89;; elp-instrument-package 90;; elp-restore-all 91;; elp-reset-function 92;; elp-reset-list 93;; elp-reset-all 94;; elp-set-master 95;; elp-unset-master 96;; elp-results 97 98;; Note that there are plenty of factors that could make the times 99;; reported unreliable, including the accuracy and granularity of your 100;; system clock, and the overhead spent in lisp calculating and 101;; recording the intervals. I figure the latter is pretty constant, 102;; so while the times may not be entirely accurate, I think they'll 103;; give you a good feel for the relative amount of work spent in the 104;; various lisp routines you are profiling. Note further that times 105;; are calculated using wall-clock time, so other system load will 106;; affect accuracy too. 107 108;;; Background: 109 110;; This program was inspired by the only two existing Emacs Lisp 111;; profilers that I'm aware of, Boaz Ben-Zvi's profile.el, and Root 112;; Boy Jim's profiler.el. Both were written for Emacs 18 and both were 113;; pretty good first shots at profiling, but I found that they didn't 114;; provide the functionality or interface that I wanted, so I wrote 115;; this. I've tested elp in XEmacs 19 and Emacs 19. There's no point 116;; in even trying to make this work with Emacs 18. 117 118;; Unlike previous profilers, elp uses Emacs 19's built-in function 119;; current-time to return interval times. This obviates the need for 120;; both an external C program and Emacs processes to communicate with 121;; such a program, and thus simplifies the package as a whole. 122 123;; TBD: 124;; Make this act like a real profiler, so that it records time spent 125;; in all branches of execution. 126 127;;; Code: 128 129 130;; start of user configuration variables 131;; vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv 132 133(defgroup elp nil 134 "Emacs Lisp Profiler." 135 :group 'lisp) 136 137(defcustom elp-function-list nil 138 "*List of functions to profile. 139Used by the command `elp-instrument-list'." 140 :type '(repeat function) 141 :group 'elp) 142 143(defcustom elp-reset-after-results t 144 "*Non-nil means reset all profiling info after results are displayed. 145Results are displayed with the `elp-results' command." 146 :type 'boolean 147 :group 'elp) 148 149(defcustom elp-sort-by-function 'elp-sort-by-total-time 150 "*Non-nil specifies elp results sorting function. 151These functions are currently available: 152 153 elp-sort-by-call-count -- sort by the highest call count 154 elp-sort-by-total-time -- sort by the highest total time 155 elp-sort-by-average-time -- sort by the highest average times 156 157You can write you're own sort function. It should adhere to the 158interface specified by the PRED argument for the `sort' defun. Each 159\"element of LIST\" is really a 4 element vector where element 0 is 160the call count, element 1 is the total time spent in the function, 161element 2 is the average time spent in the function, and element 3 is 162the symbol's name string." 163 :type 'function 164 :group 'elp) 165 166(defcustom elp-report-limit 1 167 "*Prevents some functions from being displayed in the results buffer. 168If a number, no function that has been called fewer than that number 169of times will be displayed in the output buffer. If nil, all 170functions will be displayed." 171 :type '(choice integer 172 (const :tag "Show All" nil)) 173 :group 'elp) 174 175(defcustom elp-use-standard-output nil 176 "*Non-nil says to output to `standard-output' instead of a buffer." 177 :type 'boolean 178 :group 'elp) 179 180(defcustom elp-recycle-buffers-p t 181 "*nil says to not recycle the `elp-results-buffer'. 182In other words, a new unique buffer is create every time you run 183\\[elp-results]." 184 :type 'boolean 185 :group 'elp) 186 187 188;; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 189;; end of user configuration variables 190 191 192(defvar elp-results-buffer "*ELP Profiling Results*" 193 "Buffer name for outputting profiling results.") 194 195(defconst elp-timer-info-property 'elp-info 196 "ELP information property name.") 197 198(defvar elp-all-instrumented-list nil 199 "List of all functions currently being instrumented.") 200 201(defvar elp-record-p t 202 "Controls whether functions should record times or not. 203This variable is set by the master function.") 204 205(defvar elp-master nil 206 "Master function symbol.") 207 208(defvar elp-not-profilable 209 ;; First, the functions used inside each instrumented function: 210 '(elp-wrapper called-interactively-p 211 ;; Then the functions used by the above functions. I used 212 ;; (delq nil (mapcar (lambda (x) (and (symbolp x) (fboundp x) x)) 213 ;; (aref (symbol-function 'elp-wrapper) 2))) 214 ;; to help me find this list. 215 error call-interactively apply current-time) 216 "List of functions that cannot be profiled. 217Those functions are used internally by the profiling code and profiling 218them would thus lead to infinite recursion.") 219 220(defun elp-profilable-p (fun) 221 (and (symbolp fun) 222 (fboundp fun) 223 (not (or (memq fun elp-not-profilable) 224 (keymapp fun) 225 (memq (car-safe (symbol-function fun)) '(autoload macro)) 226 (condition-case nil 227 (when (subrp (indirect-function fun)) 228 (eq 'unevalled 229 (cdr (subr-arity (indirect-function fun))))) 230 (error nil)))))) 231 232 233;;;###autoload 234(defun elp-instrument-function (funsym) 235 "Instrument FUNSYM for profiling. 236FUNSYM must be a symbol of a defined function." 237 (interactive "aFunction to instrument: ") 238 ;; restore the function. this is necessary to avoid infinite 239 ;; recursion of already instrumented functions (i.e. elp-wrapper 240 ;; calling elp-wrapper ad infinitum). it is better to simply 241 ;; restore the function than to throw an error. this will work 242 ;; properly in the face of eval-defun because if the function was 243 ;; redefined, only the timer info will be nil'd out since 244 ;; elp-restore-function is smart enough not to trash the new 245 ;; definition. 246 (elp-restore-function funsym) 247 (let* ((funguts (symbol-function funsym)) 248 (infovec (vector 0 0 funguts)) 249 (newguts '(lambda (&rest args)))) 250 ;; we cannot profile macros 251 (and (eq (car-safe funguts) 'macro) 252 (error "ELP cannot profile macro: %s" funsym)) 253 ;; TBD: at some point it might be better to load the autoloaded 254 ;; function instead of throwing an error. if we do this, then we 255 ;; probably want elp-instrument-package to be updated with the 256 ;; newly loaded list of functions. i'm not sure it's smart to do 257 ;; the autoload here, since that could have side effects, and 258 ;; elp-instrument-function is similar (in my mind) to defun-ish 259 ;; type functionality (i.e. it shouldn't execute the function). 260 (and (eq (car-safe funguts) 'autoload) 261 (error "ELP cannot profile autoloaded function: %s" funsym)) 262 ;; We cannot profile functions used internally during profiling. 263 (unless (elp-profilable-p funsym) 264 (error "ELP cannot profile the function: %s" funsym)) 265 ;; put rest of newguts together 266 (if (commandp funsym) 267 (setq newguts (append newguts '((interactive))))) 268 (setq newguts (append newguts `((elp-wrapper 269 (quote ,funsym) 270 ,(when (commandp funsym) 271 '(called-interactively-p)) 272 args)))) 273 ;; to record profiling times, we set the symbol's function 274 ;; definition so that it runs the elp-wrapper function with the 275 ;; function symbol as an argument. We place the old function 276 ;; definition on the info vector. 277 ;; 278 ;; The info vector data structure is a 3 element vector. The 0th 279 ;; element is the call-count, i.e. the total number of times this 280 ;; function has been entered. This value is bumped up on entry to 281 ;; the function so that non-local exists are still recorded. TBD: 282 ;; I haven't tested non-local exits at all, so no guarantees. 283 ;; 284 ;; The 1st element is the total amount of time in usecs that have 285 ;; been spent inside this function. This number is added to on 286 ;; function exit. 287 ;; 288 ;; The 2nd element is the old function definition list. This gets 289 ;; funcall'd in between start/end time retrievals. I believe that 290 ;; this lets us profile even byte-compiled functions. 291 292 ;; put the info vector on the property list 293 (put funsym elp-timer-info-property infovec) 294 295 ;; Set the symbol's new profiling function definition to run 296 ;; elp-wrapper. 297 (let ((advice-info (get funsym 'ad-advice-info))) 298 (if advice-info 299 (progn 300 ;; If function is advised, don't let Advice change 301 ;; its definition from under us during the `fset'. 302 (put funsym 'ad-advice-info nil) 303 (fset funsym newguts) 304 (put funsym 'ad-advice-info advice-info)) 305 (fset funsym newguts))) 306 307 ;; add this function to the instrumentation list 308 (unless (memq funsym elp-all-instrumented-list) 309 (push funsym elp-all-instrumented-list)))) 310 311(defun elp-restore-function (funsym) 312 "Restore an instrumented function to its original definition. 313Argument FUNSYM is the symbol of a defined function." 314 (interactive "aFunction to restore: ") 315 (let ((info (get funsym elp-timer-info-property))) 316 ;; delete the function from the all instrumented list 317 (setq elp-all-instrumented-list 318 (delq funsym elp-all-instrumented-list)) 319 320 ;; if the function was the master, reset the master 321 (if (eq funsym elp-master) 322 (setq elp-master nil 323 elp-record-p t)) 324 325 ;; zap the properties 326 (put funsym elp-timer-info-property nil) 327 328 ;; restore the original function definition, but if the function 329 ;; wasn't instrumented do nothing. we do this after the above 330 ;; because its possible the function got un-instrumented due to 331 ;; circumstances beyond our control. Also, check to make sure 332 ;; that the current function symbol points to elp-wrapper. If 333 ;; not, then the user probably did an eval-defun, or loaded a 334 ;; byte-compiled version, while the function was instrumented and 335 ;; we don't want to destroy the new definition. can it ever be 336 ;; the case that a lisp function can be compiled instrumented? 337 (and info 338 (functionp funsym) 339 (not (byte-code-function-p (symbol-function funsym))) 340 (assq 'elp-wrapper (symbol-function funsym)) 341 (fset funsym (aref info 2))))) 342 343;;;###autoload 344(defun elp-instrument-list (&optional list) 345 "Instrument for profiling, all functions in `elp-function-list'. 346Use optional LIST if provided instead." 347 (interactive "PList of functions to instrument: ") 348 (let ((list (or list elp-function-list))) 349 (mapcar 'elp-instrument-function list))) 350 351;;;###autoload 352(defun elp-instrument-package (prefix) 353 "Instrument for profiling, all functions which start with PREFIX. 354For example, to instrument all ELP functions, do the following: 355 356 \\[elp-instrument-package] RET elp- RET" 357 (interactive 358 (list (completing-read "Prefix of package to instrument: " 359 obarray 'elp-profilable-p))) 360 (if (zerop (length prefix)) 361 (error "Instrumenting all Emacs functions would render Emacs unusable")) 362 (elp-instrument-list 363 (mapcar 364 'intern 365 (all-completions prefix obarray 'elp-profilable-p)))) 366 367(defun elp-restore-list (&optional list) 368 "Restore the original definitions for all functions in `elp-function-list'. 369Use optional LIST if provided instead." 370 (interactive "PList of functions to restore: ") 371 (let ((list (or list elp-function-list))) 372 (mapcar 'elp-restore-function list))) 373 374(defun elp-restore-all () 375 "Restores the original definitions of all functions being profiled." 376 (interactive) 377 (elp-restore-list elp-all-instrumented-list)) 378 379 380(defun elp-reset-function (funsym) 381 "Reset the profiling information for FUNSYM." 382 (interactive "aFunction to reset: ") 383 (let ((info (get funsym elp-timer-info-property))) 384 (or info 385 (error "%s is not instrumented for profiling" funsym)) 386 (aset info 0 0) ;reset call counter 387 (aset info 1 0.0) ;reset total time 388 ;; don't muck with aref 2 as that is the old symbol definition 389 )) 390 391(defun elp-reset-list (&optional list) 392 "Reset the profiling information for all functions in `elp-function-list'. 393Use optional LIST if provided instead." 394 (interactive "PList of functions to reset: ") 395 (let ((list (or list elp-function-list))) 396 (mapcar 'elp-reset-function list))) 397 398(defun elp-reset-all () 399 "Reset the profiling information for all functions being profiled." 400 (interactive) 401 (elp-reset-list elp-all-instrumented-list)) 402 403(defun elp-set-master (funsym) 404 "Set the master function for profiling." 405 (interactive "aMaster function: ") 406 ;; when there's a master function, recording is turned off by 407 ;; default 408 (setq elp-master funsym 409 elp-record-p nil) 410 ;; make sure master function is instrumented 411 (or (memq funsym elp-all-instrumented-list) 412 (elp-instrument-function funsym))) 413 414(defun elp-unset-master () 415 "Unsets the master function." 416 (interactive) 417 ;; when there's no master function, recording is turned on by default. 418 (setq elp-master nil 419 elp-record-p t)) 420 421 422(defsubst elp-elapsed-time (start end) 423 (+ (* (- (car end) (car start)) 65536.0) 424 (- (car (cdr end)) (car (cdr start))) 425 (/ (- (car (cdr (cdr end))) (car (cdr (cdr start)))) 1000000.0))) 426 427(defun elp-wrapper (funsym interactive-p args) 428 "This function has been instrumented for profiling by the ELP. 429ELP is the Emacs Lisp Profiler. To restore the function to its 430original definition, use \\[elp-restore-function] or \\[elp-restore-all]." 431 ;; turn on recording if this is the master function 432 (if (and elp-master 433 (eq funsym elp-master)) 434 (setq elp-record-p t)) 435 ;; get info vector and original function symbol 436 (let* ((info (get funsym elp-timer-info-property)) 437 (func (aref info 2)) 438 result) 439 (or func 440 (error "%s is not instrumented for profiling" funsym)) 441 (if (not elp-record-p) 442 ;; when not recording, just call the original function symbol 443 ;; and return the results. 444 (setq result 445 (if interactive-p 446 (call-interactively func) 447 (apply func args))) 448 ;; we are recording times 449 (let (enter-time exit-time) 450 ;; increment the call-counter 451 (aset info 0 (1+ (aref info 0))) 452 ;; now call the old symbol function, checking to see if it 453 ;; should be called interactively. make sure we return the 454 ;; correct value 455 (if interactive-p 456 (setq enter-time (current-time) 457 result (call-interactively func) 458 exit-time (current-time)) 459 (setq enter-time (current-time) 460 result (apply func args) 461 exit-time (current-time))) 462 ;; calculate total time in function 463 (aset info 1 (+ (aref info 1) (elp-elapsed-time enter-time exit-time))) 464 )) 465 ;; turn off recording if this is the master function 466 (if (and elp-master 467 (eq funsym elp-master)) 468 (setq elp-record-p nil)) 469 result)) 470 471 472;; shut the byte-compiler up 473(defvar elp-field-len nil) 474(defvar elp-cc-len nil) 475(defvar elp-at-len nil) 476(defvar elp-et-len nil) 477 478(defun elp-sort-by-call-count (vec1 vec2) 479 ;; sort by highest call count. See `sort'. 480 (>= (aref vec1 0) (aref vec2 0))) 481 482(defun elp-sort-by-total-time (vec1 vec2) 483 ;; sort by highest total time spent in function. See `sort'. 484 (>= (aref vec1 1) (aref vec2 1))) 485 486(defun elp-sort-by-average-time (vec1 vec2) 487 ;; sort by highest average time spent in function. See `sort'. 488 (>= (aref vec1 2) (aref vec2 2))) 489 490(defsubst elp-pack-number (number width) 491 ;; pack the NUMBER string into WIDTH characters, watching out for 492 ;; very small or large numbers 493 (if (<= (length number) width) 494 number 495 ;; check for very large or small numbers 496 (if (string-match "^\\(.*\\)\\(e[+-].*\\)$" number) 497 (concat (substring 498 (match-string 1 number) 499 0 500 (- width (match-end 2) (- (match-beginning 2)) 3)) 501 "..." 502 (match-string 2 number)) 503 (substring number 0 width)))) 504 505(defun elp-output-result (resultvec) 506 ;; output the RESULTVEC into the results buffer. RESULTVEC is a 4 or 507 ;; more element vector where aref 0 is the call count, aref 1 is the 508 ;; total time spent in the function, aref 2 is the average time 509 ;; spent in the function, and aref 3 is the symbol's string 510 ;; name. All other elements in the vector are ignored. 511 (let* ((cc (aref resultvec 0)) 512 (tt (aref resultvec 1)) 513 (at (aref resultvec 2)) 514 (symname (aref resultvec 3)) 515 callcnt totaltime avetime) 516 (setq callcnt (number-to-string cc) 517 totaltime (number-to-string tt) 518 avetime (number-to-string at)) 519 ;; possibly prune the results 520 (if (and elp-report-limit 521 (numberp elp-report-limit) 522 (< cc elp-report-limit)) 523 nil 524 (elp-output-insert-symname symname) 525 (insert-char 32 (+ elp-field-len (- (length symname)) 2)) 526 ;; print stuff out, formatting it nicely 527 (insert callcnt) 528 (insert-char 32 (+ elp-cc-len (- (length callcnt)) 2)) 529 (let ((ttstr (elp-pack-number totaltime elp-et-len)) 530 (atstr (elp-pack-number avetime elp-at-len))) 531 (insert ttstr) 532 (insert-char 32 (+ elp-et-len (- (length ttstr)) 2)) 533 (insert atstr)) 534 (insert "\n")))) 535 536(defvar elp-results-symname-map 537 (let ((map (make-sparse-keymap))) 538 (define-key map [mouse-2] 'elp-results-jump-to-definition) 539 (define-key map "\C-m" 'elp-results-jump-to-definition) 540 map) 541 "Keymap used on the function name column." ) 542 543(defun elp-results-jump-to-definition (&optional event) 544 "Jump to the definition of the function under the point." 545 (interactive (list last-nonmenu-event)) 546 (if event (posn-set-point (event-end event))) 547 (find-function (get-text-property (point) 'elp-symname))) 548 549(defun elp-output-insert-symname (symname) 550 ;; Insert SYMNAME with text properties. 551 (insert (propertize symname 552 'elp-symname (intern symname) 553 'keymap elp-results-symname-map 554 'mouse-face 'highlight 555 'help-echo "mouse-2 or RET jumps to definition"))) 556 557;;;###autoload 558(defun elp-results () 559 "Display current profiling results. 560If `elp-reset-after-results' is non-nil, then current profiling 561information for all instrumented functions are reset after results are 562displayed." 563 (interactive) 564 (let ((curbuf (current-buffer)) 565 (resultsbuf (if elp-recycle-buffers-p 566 (get-buffer-create elp-results-buffer) 567 (generate-new-buffer elp-results-buffer)))) 568 (set-buffer resultsbuf) 569 (erase-buffer) 570 ;; get the length of the longest function name being profiled 571 (let* ((longest 0) 572 (title "Function Name") 573 (titlelen (length title)) 574 (elp-field-len titlelen) 575 (cc-header "Call Count") 576 (elp-cc-len (length cc-header)) 577 (et-header "Elapsed Time") 578 (elp-et-len (length et-header)) 579 (at-header "Average Time") 580 (elp-at-len (length at-header)) 581 (resvec 582 (mapcar 583 (function 584 (lambda (funsym) 585 (let* ((info (get funsym elp-timer-info-property)) 586 (symname (format "%s" funsym)) 587 (cc (aref info 0)) 588 (tt (aref info 1))) 589 (if (not info) 590 (insert "No profiling information found for: " 591 symname) 592 (setq longest (max longest (length symname))) 593 (vector cc tt (if (zerop cc) 594 0.0 ;avoid arithmetic div-by-zero errors 595 (/ (float tt) (float cc))) 596 symname))))) 597 elp-all-instrumented-list)) 598 ) ; end let* 599 (insert title) 600 (if (> longest titlelen) 601 (progn 602 (insert-char 32 (- longest titlelen)) 603 (setq elp-field-len longest))) 604 (insert " " cc-header " " et-header " " at-header "\n") 605 (insert-char ?= elp-field-len) 606 (insert " ") 607 (insert-char ?= elp-cc-len) 608 (insert " ") 609 (insert-char ?= elp-et-len) 610 (insert " ") 611 (insert-char ?= elp-at-len) 612 (insert "\n") 613 ;; if sorting is enabled, then sort the results list. in either 614 ;; case, call elp-output-result to output the result in the 615 ;; buffer 616 (if elp-sort-by-function 617 (setq resvec (sort resvec elp-sort-by-function))) 618 (mapcar 'elp-output-result resvec)) 619 ;; now pop up results buffer 620 (set-buffer curbuf) 621 (pop-to-buffer resultsbuf) 622 ;; copy results to standard-output? 623 (if (or elp-use-standard-output noninteractive) 624 (princ (buffer-substring (point-min) (point-max)))) 625 ;; reset profiling info if desired 626 (and elp-reset-after-results 627 (elp-reset-all)))) 628 629(defun elp-unload-hook () 630 (elp-restore-all)) 631(add-hook 'elp-unload-hook 'elp-unload-hook) 632 633(provide 'elp) 634 635;; arch-tag: c4eef311-9b3e-4bb2-8a54-3485d41b4eb1 636;;; elp.el ends here 637