1 /*
2 * Copyright 2005, Axel Dörfler, axeld@pinc-software.de. All rights reserved.
3 * Distributed under the terms of the MIT License.
4 */
5
6 /** This module memorizes all opened files for a certain session. A session
7 * can be the start of an application or the boot process.
8 * When a session is started, it will prefetch all files from an earlier
9 * session in order to speed up the launching or booting process.
10 *
11 * Note: this module is using private kernel API and is definitely not
12 * meant to be an example on how to write modules.
13 */
14
15
16 #include "launch_speedup.h"
17
18 #include <KernelExport.h>
19 #include <Node.h>
20
21 #include <util/kernel_cpp.h>
22 #include <util/AutoLock.h>
23 #include <thread.h>
24 #include <team.h>
25 #include <file_cache.h>
26 #include <generic_syscall.h>
27 #include <syscalls.h>
28
29 #include <unistd.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <stdio.h>
33 #include <errno.h>
34 #include <ctype.h>
35
36 extern dev_t gBootDevice;
37
38
39 // ToDo: combine the last 3-5 sessions to their intersection
40 // ToDo: maybe ignore sessions if the node count is < 3 (without system libs)
41
42 #define TRACE_CACHE_MODULE
43 #ifdef TRACE_CACHE_MODULE
44 # define TRACE(x) dprintf x
45 #else
46 # define TRACE(x) ;
47 #endif
48
49 #define VNODE_HASH(mountid, vnodeid) (((uint32)((vnodeid) >> 32) \
50 + (uint32)(vnodeid)) ^ (uint32)(mountid))
51
52 struct data_part {
53 off_t offset;
54 off_t size;
55 };
56
57 struct node {
58 struct node *next;
59 node_ref ref;
60 int32 ref_count;
61 bigtime_t timestamp;
62 data_part parts[5];
63 size_t part_count;
64 };
65
66 struct NodeHash {
67 typedef node_ref KeyType;
68 typedef node ValueType;
69
HashKeyNodeHash70 size_t HashKey(KeyType key) const
71 {
72 return VNODE_HASH(key.device, key.node);
73 }
74
HashNodeHash75 size_t Hash(ValueType* value) const
76 {
77 return HashKey(value->ref);
78 }
79
CompareNodeHash80 bool Compare(KeyType key, ValueType* node) const
81 {
82 return (node->ref.device == key.device && node->ref.node == key.node);
83 }
84
GetLinkNodeHash85 ValueType*& GetLink(ValueType* value) const
86 {
87 return value->next;
88 }
89 };
90
91 typedef BOpenHashTable<NodeHash> NodeTable;
92
93 class Session {
94 public:
95 Session(team_id team, const char *name, dev_t device,
96 ino_t node, int32 seconds);
97 Session(const char *name);
98 ~Session();
99
100 status_t InitCheck();
Team() const101 team_id Team() const { return fTeam; }
Name() const102 const char *Name() const { return fName; }
NodeRef() const103 const node_ref &NodeRef() const { return fNodeRef; }
IsActive() const104 bool IsActive() const { return fActiveUntil >= system_time(); }
IsClosing() const105 bool IsClosing() const { return fClosing; }
106 bool IsMainSession() const;
107 bool IsWorthSaving() const;
108
109 void AddNode(dev_t device, ino_t node);
110 void RemoveNode(dev_t device, ino_t node);
111
Lock()112 void Lock() { mutex_lock(&fLock); }
Unlock()113 void Unlock() { mutex_unlock(&fLock); }
114
115 status_t StartWatchingTeam();
116 void StopWatchingTeam();
117
118 status_t LoadFromDirectory(int fd);
119 status_t Save();
120 void Prefetch();
121
Next()122 Session *&Next() { return fNext; }
123
124 private:
125 struct node *_FindNode(dev_t device, ino_t node);
126
127 Session *fNext;
128 char fName[B_OS_NAME_LENGTH];
129 mutex fLock;
130 NodeTable *fNodeHash;
131 struct node *fNodes;
132 int32 fNodeCount;
133 team_id fTeam;
134 node_ref fNodeRef;
135 bigtime_t fActiveUntil;
136 bigtime_t fTimestamp;
137 bool fClosing;
138 bool fIsWatchingTeam;
139 };
140
141 class SessionGetter {
142 public:
143 SessionGetter(team_id team, Session **_session);
144 ~SessionGetter();
145
146 status_t New(const char *name, dev_t device, ino_t node,
147 Session **_session);
148 void Stop();
149
150 private:
151 Session *fSession;
152 };
153
154 static Session *sMainSession;
155 static SessionTable *sTeamHash;
156 static PrefetchTable *sPrefetchHash;
157 static Session *sMainPrefetchSessions;
158 // singly-linked list
159 static recursive_lock sLock;
160
161
node_ref()162 node_ref::node_ref()
163 {
164 // part of libbe.so
165 }
166
167
168 struct PrefetchHash {
169 typedef node_ref KeyType;
170 typedef Session ValueType;
171
HashKeyPrefetchHash172 size_t HashKey(KeyType key) const
173 {
174 return VNODE_HASH(key.device, key.node);
175 }
176
HashPrefetchHash177 size_t Hash(ValueType* value) const
178 {
179 return HashKey(value->NodeRef());
180 }
181
ComparePrefetchHash182 bool Compare(KeyType key, ValueType* session) const
183 {
184 return (session->NodeRef().device == key.device
185 && session->NodeRef().node == key.node);
186 }
187
GetLinkPrefetchHash188 ValueType*& GetLink(ValueType* value) const
189 {
190 return value->Next();
191 }
192 };
193
194 typedef BOpenHashTable<PrefetchHash> PrefetchTable;
195
196
197 struct SessionHash {
198 typedef team_id KeyType;
199 typedef Session ValueType;
200
HashKeySessionHash201 size_t HashKey(KeyType key) const
202 {
203 return key;
204 }
205
HashSessionHash206 size_t Hash(ValueType* value) const
207 {
208 return HashKey(value->Team());
209 }
210
CompareSessionHash211 bool Compare(KeyType key, ValueType* session) const
212 {
213 return session->Team == key;
214 }
215
GetLinkSessionHash216 ValueType*& GetLink(ValueType* value) const
217 {
218 return value->Next();
219 }
220 };
221
222 typedef BOpenHashTable<SessionHash> SessionTable;
223
224
225 static void
stop_session(Session * session)226 stop_session(Session *session)
227 {
228 if (session == NULL)
229 return;
230
231 TRACE(("stop_session(%s)\n", session->Name()));
232
233 if (session->IsWorthSaving())
234 session->Save();
235
236 {
237 RecursiveLocker locker(&sLock);
238
239 if (session->Team() >= B_OK)
240 sTeamHash->Remove(session);
241
242 if (session == sMainSession)
243 sMainSession = NULL;
244 }
245
246 delete session;
247 }
248
249
250 static Session *
start_session(team_id team,dev_t device,ino_t node,const char * name,int32 seconds=30)251 start_session(team_id team, dev_t device, ino_t node, const char *name,
252 int32 seconds = 30)
253 {
254 RecursiveLocker locker(&sLock);
255
256 Session *session = new Session(team, name, device, node, seconds);
257 if (session == NULL)
258 return NULL;
259
260 if (session->InitCheck() != B_OK || session->StartWatchingTeam() != B_OK) {
261 delete session;
262 return NULL;
263 }
264
265 // let's see if there is a prefetch session for this session
266
267 Session *prefetchSession;
268 if (session->IsMainSession()) {
269 // search for session by name
270 for (prefetchSession = sMainPrefetchSessions;
271 prefetchSession != NULL;
272 prefetchSession = prefetchSession->Next()) {
273 if (!strcmp(prefetchSession->Name(), name)) {
274 // found session!
275 break;
276 }
277 }
278 } else {
279 // ToDo: search for session by device/node ID
280 prefetchSession = NULL;
281 }
282 if (prefetchSession != NULL) {
283 TRACE(("found prefetch session %s\n", prefetchSession->Name()));
284 prefetchSession->Prefetch();
285 }
286
287 if (team >= B_OK)
288 sTeamHash->Insert(session);
289
290 session->Lock();
291 return session;
292 }
293
294
295 static void
team_gone(team_id team,void * _session)296 team_gone(team_id team, void *_session)
297 {
298 Session *session = (Session *)_session;
299
300 session->Lock();
301 stop_session(session);
302 }
303
304
305 static bool
parse_node_ref(const char * string,node_ref & ref,const char ** _end=NULL)306 parse_node_ref(const char *string, node_ref &ref, const char **_end = NULL)
307 {
308 // parse node ref
309 char *end;
310 ref.device = strtol(string, &end, 0);
311 if (end == NULL || ref.device == 0)
312 return false;
313
314 ref.node = strtoull(end + 1, &end, 0);
315
316 if (_end)
317 *_end = end;
318 return true;
319 }
320
321
322 static struct node *
new_node(dev_t device,ino_t id)323 new_node(dev_t device, ino_t id)
324 {
325 struct node *node = new ::node;
326 if (node == NULL)
327 return NULL;
328
329 node->ref.device = device;
330 node->ref.node = id;
331 node->timestamp = system_time();
332
333 return node;
334 }
335
336
337 static void
load_prefetch_data()338 load_prefetch_data()
339 {
340 DIR *dir = opendir("/etc/launch_cache");
341 if (dir == NULL)
342 return;
343
344 struct dirent *dirent;
345 while ((dirent = readdir(dir)) != NULL) {
346 if (dirent->d_name[0] == '.')
347 continue;
348
349 Session *session = new Session(dirent->d_name);
350
351 if (session->LoadFromDirectory(dirfd(dir)) != B_OK) {
352 delete session;
353 continue;
354 }
355
356 if (session->IsMainSession()) {
357 session->Next() = sMainPrefetchSessions;
358 sMainPrefetchSessions = session;
359 } else {
360 sPrefetchHash->Insert(session);
361 }
362 }
363
364 closedir(dir);
365 }
366
367
368 // #pragma mark -
369
370
Session(team_id team,const char * name,dev_t device,ino_t node,int32 seconds)371 Session::Session(team_id team, const char *name, dev_t device,
372 ino_t node, int32 seconds)
373 :
374 fNodes(NULL),
375 fNodeCount(0),
376 fTeam(team),
377 fClosing(false),
378 fIsWatchingTeam(false)
379 {
380 if (name != NULL) {
381 size_t length = strlen(name) + 1;
382 if (length > B_OS_NAME_LENGTH)
383 name += length - B_OS_NAME_LENGTH;
384
385 strlcpy(fName, name, B_OS_NAME_LENGTH);
386 } else
387 fName[0] = '\0';
388
389 mutex_init(&fLock, "launch speedup session");
390 fNodeHash = new(std::nothrow) NodeTable();
391 if (fNodeHash && fNodeHash->Init(64) != B_OK) {
392 delete fNodeHash;
393 fNodeHash = NULL;
394 }
395 fActiveUntil = system_time() + seconds * 1000000LL;
396 fTimestamp = system_time();
397
398 fNodeRef.device = device;
399 fNodeRef.node = node;
400
401 TRACE(("start session %ld:%lld \"%s\", system_time: %lld, active until: %lld\n",
402 device, node, Name(), system_time(), fActiveUntil));
403 }
404
405
Session(const char * name)406 Session::Session(const char *name)
407 :
408 fNodeHash(NULL),
409 fNodes(NULL),
410 fClosing(false),
411 fIsWatchingTeam(false)
412 {
413 fTeam = -1;
414 fNodeRef.device = -1;
415 fNodeRef.node = -1;
416
417 if (isdigit(name[0]))
418 parse_node_ref(name, fNodeRef);
419
420 strlcpy(fName, name, B_OS_NAME_LENGTH);
421 }
422
423
~Session()424 Session::~Session()
425 {
426 mutex_destroy(&fLock);
427
428 // free all nodes
429 struct node *node, *next = NULL;
430
431 if (fNodeHash) {
432 // ... from the hash
433 node = fNodeHash->Clear(true);
434 } else {
435 // ... from the list
436 node = fNodes;
437 }
438
439 for (; node != NULL; node = next) {
440 next = node->next;
441 free(node);
442 }
443
444 delete fNodeHash;
445 StopWatchingTeam();
446 }
447
448
449 status_t
InitCheck()450 Session::InitCheck()
451 {
452 if (fNodeHash == NULL)
453 return B_NO_MEMORY;
454
455 return B_OK;
456 }
457
458
459 node *
_FindNode(dev_t device,ino_t node)460 Session::_FindNode(dev_t device, ino_t node)
461 {
462 node_ref key;
463 key.device = device;
464 key.node = node;
465
466 return fNodeHash->Lookup(key);
467 }
468
469
470 void
AddNode(dev_t device,ino_t id)471 Session::AddNode(dev_t device, ino_t id)
472 {
473 struct node *node = _FindNode(device, id);
474 if (node != NULL) {
475 node->ref_count++;
476 return;
477 }
478
479 node = new_node(device, id);
480 if (node == NULL)
481 return;
482
483 fNodeHash->Insert(node);
484 fNodeCount++;
485 }
486
487
488 void
RemoveNode(dev_t device,ino_t id)489 Session::RemoveNode(dev_t device, ino_t id)
490 {
491 struct node *node = _FindNode(device, id);
492 if (node != NULL && --node->ref_count <= 0) {
493 fNodeHash->Remove(node);
494 fNodeCount--;
495 }
496 }
497
498
499 status_t
StartWatchingTeam()500 Session::StartWatchingTeam()
501 {
502 if (Team() < B_OK)
503 return B_OK;
504
505 status_t status = start_watching_team(Team(), team_gone, this);
506 if (status == B_OK)
507 fIsWatchingTeam = true;
508
509 return status;
510 }
511
512
513 void
StopWatchingTeam()514 Session::StopWatchingTeam()
515 {
516 if (fIsWatchingTeam)
517 stop_watching_team(Team(), team_gone, this);
518 }
519
520
521 void
Prefetch()522 Session::Prefetch()
523 {
524 if (fNodes == NULL || fNodeHash != NULL)
525 return;
526
527 for (struct node *node = fNodes; node != NULL; node = node->next) {
528 cache_prefetch(node->ref.device, node->ref.node, 0, ~0UL);
529 }
530 }
531
532
533 status_t
LoadFromDirectory(int directoryFD)534 Session::LoadFromDirectory(int directoryFD)
535 {
536 TRACE(("load session %s\n", Name()));
537
538 int fd = _kern_open(directoryFD, Name(), O_RDONLY, 0);
539 if (fd < B_OK)
540 return fd;
541
542 struct stat stat;
543 if (fstat(fd, &stat) != 0) {
544 close(fd);
545 return errno;
546 }
547
548 if (stat.st_size > 32768) {
549 // for safety reasons
550 // ToDo: make a bit larger later
551 close(fd);
552 return B_BAD_DATA;
553 }
554
555 char *buffer = (char *)malloc(stat.st_size);
556 if (buffer == NULL) {
557 close(fd);
558 return B_NO_MEMORY;
559 }
560
561 if (read(fd, buffer, stat.st_size) < stat.st_size) {
562 free(buffer);
563 close(fd);
564 return B_ERROR;
565 }
566
567 const char *line = buffer;
568 node_ref nodeRef;
569 while (parse_node_ref(line, nodeRef, &line)) {
570 struct node *node = new_node(nodeRef.device, nodeRef.node);
571 if (node != NULL) {
572 // note: this reverses the order of the nodes in the file
573 node->next = fNodes;
574 fNodes = node;
575 }
576 line++;
577 }
578
579 free(buffer);
580 close(fd);
581 return B_OK;
582 }
583
584
585 status_t
Save()586 Session::Save()
587 {
588 fClosing = true;
589
590 char name[B_OS_NAME_LENGTH + 25];
591 if (!IsMainSession()) {
592 snprintf(name, sizeof(name), "/etc/launch_cache/%ld:%lld %s",
593 fNodeRef.device, fNodeRef.node, Name());
594 } else
595 snprintf(name, sizeof(name), "/etc/launch_cache/%s", Name());
596
597 int fd = open(name, O_CREAT | O_TRUNC | O_WRONLY, 0644);
598 if (fd < B_OK)
599 return errno;
600
601 status_t status = B_OK;
602 off_t fileSize = 0;
603
604 // ToDo: order nodes by timestamp... (should improve launch speed)
605 // ToDo: test which parts of a file have been read (and save that as well)
606
607 // enlarge file, so that it can be written faster
608 ftruncate(fd, 512 * 1024);
609
610 NodeTable::Iterator iterator(fNodeHash);
611 while (iterator.HasNext()) {
612 struct node *node = iterator.Next();
613 snprintf(name, sizeof(name), "%ld:%lld\n", node->ref.device, node->ref.node);
614
615 ssize_t bytesWritten = write(fd, name, strlen(name));
616 if (bytesWritten < B_OK) {
617 status = bytesWritten;
618 break;
619 }
620
621 fileSize += bytesWritten;
622 }
623
624 ftruncate(fd, fileSize);
625 close(fd);
626
627 return status;
628 }
629
630
631 bool
IsWorthSaving() const632 Session::IsWorthSaving() const
633 {
634 // ToDo: sort out entries with only very few nodes, and those that load
635 // instantly, anyway
636 if (fNodeCount < 5 || system_time() - fTimestamp < 400000) {
637 // sort anything out that opens less than 5 files, or needs less
638 // than 0.4 seconds to load an run
639 return false;
640 }
641 return true;
642 }
643
644
645 bool
IsMainSession() const646 Session::IsMainSession() const
647 {
648 return fNodeRef.node == -1;
649 }
650
651
652 // #pragma mark -
653
654
SessionGetter(team_id team,Session ** _session)655 SessionGetter::SessionGetter(team_id team, Session **_session)
656 {
657 RecursiveLocker locker(&sLock);
658
659 if (sMainSession != NULL)
660 fSession = sMainSession;
661 else
662 fSession = sTeamHash->Lookup(team);
663
664 if (fSession != NULL) {
665 if (!fSession->IsClosing())
666 fSession->Lock();
667 else
668 fSession = NULL;
669 }
670
671 *_session = fSession;
672 }
673
674
~SessionGetter()675 SessionGetter::~SessionGetter()
676 {
677 if (fSession != NULL)
678 fSession->Unlock();
679 }
680
681
682 status_t
New(const char * name,dev_t device,ino_t node,Session ** _session)683 SessionGetter::New(const char *name, dev_t device, ino_t node,
684 Session **_session)
685 {
686 Thread *thread = thread_get_current_thread();
687 fSession = start_session(thread->team->id, device, node, name);
688
689 if (fSession != NULL) {
690 *_session = fSession;
691 return B_OK;
692 }
693
694 return B_ERROR;
695 }
696
697
698 void
Stop()699 SessionGetter::Stop()
700 {
701 if (fSession == sMainSession)
702 sMainSession = NULL;
703
704 stop_session(fSession);
705 fSession = NULL;
706 }
707
708 // #pragma mark -
709
710
711 static void
node_opened(struct vnode * vnode,dev_t device,ino_t parent,ino_t node,const char * name,off_t size)712 node_opened(struct vnode *vnode, dev_t device, ino_t parent,
713 ino_t node, const char *name, off_t size)
714 {
715 if (device < gBootDevice) {
716 // we ignore any access to rootfs, pipefs, and devfs
717 // ToDo: if we can ever move the boot device on the fly, this will break
718 return;
719 }
720
721 Session *session;
722 SessionGetter getter(team_get_current_team_id(), &session);
723
724 if (session == NULL) {
725 char buffer[B_FILE_NAME_LENGTH];
726 if (name == NULL
727 && vfs_get_vnode_name(vnode, buffer, sizeof(buffer)) == B_OK)
728 name = buffer;
729
730 // create new session for this team
731 getter.New(name, device, node, &session);
732 }
733
734 if (session == NULL || !session->IsActive()) {
735 if (sMainSession != NULL) {
736 // ToDo: this opens a race condition with the "stop session" syscall
737 getter.Stop();
738 }
739 return;
740 }
741
742 session->AddNode(device, node);
743 }
744
745
746 static void
node_closed(struct vnode * vnode,dev_t device,ino_t node,int32 accessType)747 node_closed(struct vnode *vnode, dev_t device, ino_t node,
748 int32 accessType)
749 {
750 Session *session;
751 SessionGetter getter(team_get_current_team_id(), &session);
752
753 if (session == NULL)
754 return;
755
756 if (accessType == FILE_CACHE_NO_IO)
757 session->RemoveNode(device, node);
758 }
759
760
761 static status_t
launch_speedup_control(const char * subsystem,uint32 function,void * buffer,size_t bufferSize)762 launch_speedup_control(const char *subsystem, uint32 function,
763 void *buffer, size_t bufferSize)
764 {
765 switch (function) {
766 case LAUNCH_SPEEDUP_START_SESSION:
767 {
768 char name[B_OS_NAME_LENGTH];
769 if (!IS_USER_ADDRESS(buffer)
770 || user_strlcpy(name, (const char *)buffer, B_OS_NAME_LENGTH) < B_OK)
771 return B_BAD_ADDRESS;
772
773 if (isdigit(name[0]) || name[0] == '.')
774 return B_BAD_VALUE;
775
776 sMainSession = start_session(-1, -1, -1, name, 60);
777 sMainSession->Unlock();
778 return B_OK;
779 }
780
781 case LAUNCH_SPEEDUP_STOP_SESSION:
782 {
783 char name[B_OS_NAME_LENGTH];
784 if (!IS_USER_ADDRESS(buffer)
785 || user_strlcpy(name, (const char *)buffer, B_OS_NAME_LENGTH) < B_OK)
786 return B_BAD_ADDRESS;
787
788 // ToDo: this check is not thread-safe
789 if (sMainSession == NULL || strcmp(sMainSession->Name(), name))
790 return B_BAD_VALUE;
791
792 if (!strcmp(name, "system boot"))
793 dprintf("STOP BOOT %lld\n", system_time());
794
795 sMainSession->Lock();
796 stop_session(sMainSession);
797 sMainSession = NULL;
798 return B_OK;
799 }
800 }
801
802 return B_BAD_VALUE;
803 }
804
805
806 static void
uninit()807 uninit()
808 {
809 unregister_generic_syscall(LAUNCH_SPEEDUP_SYSCALLS, 1);
810
811 recursive_lock_lock(&sLock);
812
813 // free all sessions from the hashes
814
815 Session *session = sTeamHash->Clear(true);
816 while (session != NULL) {
817 Session *next = session->next;
818 delete session;
819 session = next;
820 }
821 session = sPrefetchHash->Clear(true);
822 while (session != NULL) {
823 Session *next = session->next;
824 delete session;
825 session = next;
826 }
827
828 // free all sessions from the main prefetch list
829
830 for (session = sMainPrefetchSessions; session != NULL; ) {
831 sMainPrefetchSessions = session->Next();
832 delete session;
833 session = sMainPrefetchSessions;
834 }
835
836 delete sTeamHash;
837 delete sPrefetchHash;
838 recursive_lock_destroy(&sLock);
839 }
840
841
842 static status_t
init()843 init()
844 {
845 sTeamHash = new(std::nothrow) SessionTable();
846 if (sTeamHash == NULL || sTeamHash->Init(64) != B_OK)
847 return B_NO_MEMORY;
848
849 status_t status;
850
851 sPrefetchHash = new(std::nothrow) PrefetchTable();
852 if (sPrefetchHash == NULL || sPrefetchHash->Init(64) != B_OK) {
853 status = B_NO_MEMORY;
854 goto err1;
855 }
856
857 recursive_lock_init(&sLock, "launch speedup");
858
859 // register kernel syscalls
860 if (register_generic_syscall(LAUNCH_SPEEDUP_SYSCALLS,
861 launch_speedup_control, 1, 0) != B_OK) {
862 status = B_ERROR;
863 goto err3;
864 }
865
866 // read in prefetch knowledge base
867
868 mkdir("/etc/launch_cache", 0755);
869 load_prefetch_data();
870
871 // start boot session
872
873 sMainSession = start_session(-1, -1, -1, "system boot");
874 sMainSession->Unlock();
875 dprintf("START BOOT %lld\n", system_time());
876 return B_OK;
877
878 err3:
879 recursive_lock_destroy(&sLock);
880 delete sPrefetchHash;
881 err1:
882 delete sTeamHash;
883 return status;
884 }
885
886
887 static status_t
std_ops(int32 op,...)888 std_ops(int32 op, ...)
889 {
890 switch (op) {
891 case B_MODULE_INIT:
892 return init();
893
894 case B_MODULE_UNINIT:
895 uninit();
896 return B_OK;
897
898 default:
899 return B_ERROR;
900 }
901 }
902
903
904 static struct cache_module_info sLaunchSpeedupModule = {
905 {
906 CACHE_MODULES_NAME "/launch_speedup/v1",
907 0,
908 std_ops,
909 },
910 node_opened,
911 node_closed,
912 NULL,
913 };
914
915
916 module_info *modules[] = {
917 (module_info *)&sLaunchSpeedupModule,
918 NULL
919 };
920