root/src/add-ons/kernel/file_cache/launch_speedup.cpp
/*
 * Copyright 2005, Axel Dörfler, axeld@pinc-software.de. All rights reserved.
 * Distributed under the terms of the MIT License.
 */

/** This module memorizes all opened files for a certain session. A session
 *      can be the start of an application or the boot process.
 *      When a session is started, it will prefetch all files from an earlier
 *      session in order to speed up the launching or booting process.
 *
 *      Note: this module is using private kernel API and is definitely not
 *              meant to be an example on how to write modules.
 */


#include "launch_speedup.h"

#include <KernelExport.h>
#include <Node.h>

#include <util/kernel_cpp.h>
#include <util/AutoLock.h>
#include <thread.h>
#include <team.h>
#include <file_cache.h>
#include <generic_syscall.h>
#include <syscalls.h>

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <ctype.h>

extern dev_t gBootDevice;


// ToDo: combine the last 3-5 sessions to their intersection
// ToDo: maybe ignore sessions if the node count is < 3 (without system libs)

#define TRACE_CACHE_MODULE
#ifdef TRACE_CACHE_MODULE
#       define TRACE(x) dprintf x
#else
#       define TRACE(x) ;
#endif

#define VNODE_HASH(mountid, vnodeid) (((uint32)((vnodeid) >> 32) \
        + (uint32)(vnodeid)) ^ (uint32)(mountid))

struct data_part {
        off_t           offset;
        off_t           size;
};

struct node {
        struct node     *next;
        node_ref        ref;
        int32           ref_count;
        bigtime_t       timestamp;
        data_part       parts[5];
        size_t          part_count;
};

struct NodeHash {
        typedef node_ref        KeyType;
        typedef node            ValueType;

        size_t HashKey(KeyType key) const
        {
                return VNODE_HASH(key.device, key.node);
        }

        size_t Hash(ValueType* value) const
        {
                return HashKey(value->ref);
        }

        bool Compare(KeyType key, ValueType* node) const
        {
                return (node->ref.device == key.device && node->ref.node == key.node);
        }

        ValueType*& GetLink(ValueType* value) const
        {
                return value->next;
        }
};

typedef BOpenHashTable<NodeHash> NodeTable;

class Session {
        public:
                Session(team_id team, const char *name, dev_t device,
                        ino_t node, int32 seconds);
                Session(const char *name);
                ~Session();

                status_t InitCheck();
                team_id Team() const { return fTeam; }
                const char *Name() const { return fName; }
                const node_ref &NodeRef() const { return fNodeRef; }
                bool IsActive() const { return fActiveUntil >= system_time(); }
                bool IsClosing() const { return fClosing; }
                bool IsMainSession() const;
                bool IsWorthSaving() const;

                void AddNode(dev_t device, ino_t node);
                void RemoveNode(dev_t device, ino_t node);

                void Lock() { mutex_lock(&fLock); }
                void Unlock() { mutex_unlock(&fLock); }

                status_t StartWatchingTeam();
                void StopWatchingTeam();

                status_t LoadFromDirectory(int fd);
                status_t Save();
                void Prefetch();

                Session *&Next() { return fNext; }

        private:
                struct node *_FindNode(dev_t device, ino_t node);

                Session         *fNext;
                char            fName[B_OS_NAME_LENGTH];
                mutex           fLock;
                NodeTable       *fNodeHash;
                struct node     *fNodes;
                int32           fNodeCount;
                team_id         fTeam;
                node_ref        fNodeRef;
                bigtime_t       fActiveUntil;
                bigtime_t       fTimestamp;
                bool            fClosing;
                bool            fIsWatchingTeam;
};

class SessionGetter {
        public:
                SessionGetter(team_id team, Session **_session);
                ~SessionGetter();

                status_t New(const char *name, dev_t device, ino_t node,
                                        Session **_session);
                void Stop();

        private:
                Session *fSession;
};

static Session *sMainSession;
static SessionTable *sTeamHash;
static PrefetchTable *sPrefetchHash;
static Session *sMainPrefetchSessions;
        // singly-linked list
static recursive_lock sLock;


node_ref::node_ref()
{
        // part of libbe.so
}


struct PrefetchHash {
        typedef node_ref        KeyType;
        typedef Session         ValueType;

        size_t HashKey(KeyType key) const
        {
                return VNODE_HASH(key.device, key.node);
        }

        size_t Hash(ValueType* value) const
        {
                return HashKey(value->NodeRef());
        }

        bool Compare(KeyType key, ValueType* session) const
        {
                return (session->NodeRef().device == key.device
                        && session->NodeRef().node == key.node);
        }

        ValueType*& GetLink(ValueType* value) const
        {
                return value->Next();
        }
};

typedef BOpenHashTable<PrefetchHash> PrefetchTable;


struct SessionHash {
        typedef team_id         KeyType;
        typedef Session         ValueType;

        size_t HashKey(KeyType key) const
        {
                return key;
        }

        size_t Hash(ValueType* value) const
        {
                return HashKey(value->Team());
        }

        bool Compare(KeyType key, ValueType* session) const
        {
                return session->Team == key;
        }

        ValueType*& GetLink(ValueType* value) const
        {
                return value->Next();
        }
};

typedef BOpenHashTable<SessionHash> SessionTable;


static void
stop_session(Session *session)
{
        if (session == NULL)
                return;

        TRACE(("stop_session(%s)\n", session->Name()));

        if (session->IsWorthSaving())
                session->Save();

        {
                RecursiveLocker locker(&sLock);

                if (session->Team() >= B_OK)
                        sTeamHash->Remove(session);

                if (session == sMainSession)
                        sMainSession = NULL;
        }

        delete session;
}


static Session *
start_session(team_id team, dev_t device, ino_t node, const char *name,
        int32 seconds = 30)
{
        RecursiveLocker locker(&sLock);

        Session *session = new Session(team, name, device, node, seconds);
        if (session == NULL)
                return NULL;

        if (session->InitCheck() != B_OK || session->StartWatchingTeam() != B_OK) {
                delete session;
                return NULL;
        }

        // let's see if there is a prefetch session for this session

        Session *prefetchSession;
        if (session->IsMainSession()) {
                // search for session by name
                for (prefetchSession = sMainPrefetchSessions;
                                prefetchSession != NULL;
                                prefetchSession = prefetchSession->Next()) {
                        if (!strcmp(prefetchSession->Name(), name)) {
                                // found session!
                                break;
                        }
                }
        } else {
                // ToDo: search for session by device/node ID
                prefetchSession = NULL;
        }
        if (prefetchSession != NULL) {
                TRACE(("found prefetch session %s\n", prefetchSession->Name()));
                prefetchSession->Prefetch();
        }

        if (team >= B_OK)
                sTeamHash->Insert(session);

        session->Lock();
        return session;
}


static void
team_gone(team_id team, void *_session)
{
        Session *session = (Session *)_session;

        session->Lock();
        stop_session(session);
}


static bool
parse_node_ref(const char *string, node_ref &ref, const char **_end = NULL)
{
        // parse node ref
        char *end;
        ref.device = strtol(string, &end, 0);
        if (end == NULL || ref.device == 0)
                return false;

        ref.node = strtoull(end + 1, &end, 0);

        if (_end)
                *_end = end;
        return true;
}


static struct node *
new_node(dev_t device, ino_t id)
{
        struct node *node = new ::node;
        if (node == NULL)
                return NULL;

        node->ref.device = device;
        node->ref.node = id;
        node->timestamp = system_time();

        return node;
}


static void
load_prefetch_data()
{
        DIR *dir = opendir("/etc/launch_cache");
        if (dir == NULL)
                return;

        struct dirent *dirent;
        while ((dirent = readdir(dir)) != NULL) {
                if (dirent->d_name[0] == '.')
                        continue;

                Session *session = new Session(dirent->d_name);

                if (session->LoadFromDirectory(dirfd(dir)) != B_OK) {
                        delete session;
                        continue;
                }

                if (session->IsMainSession()) {
                        session->Next() = sMainPrefetchSessions;
                        sMainPrefetchSessions = session;
                } else {
                        sPrefetchHash->Insert(session);
                }
        }

        closedir(dir);
}


//      #pragma mark -


Session::Session(team_id team, const char *name, dev_t device,
        ino_t node, int32 seconds)
        :
        fNodes(NULL),
        fNodeCount(0),
        fTeam(team),
        fClosing(false),
        fIsWatchingTeam(false)
{
        if (name != NULL) {
                size_t length = strlen(name) + 1;
                if (length > B_OS_NAME_LENGTH)
                        name += length - B_OS_NAME_LENGTH;

                strlcpy(fName, name, B_OS_NAME_LENGTH);
        } else
                fName[0] = '\0';

        mutex_init(&fLock, "launch speedup session");
        fNodeHash = new(std::nothrow) NodeTable();
        if (fNodeHash && fNodeHash->Init(64) != B_OK) {
                delete fNodeHash;
                fNodeHash = NULL;
        }
        fActiveUntil = system_time() + seconds * 1000000LL;
        fTimestamp = system_time();

        fNodeRef.device = device;
        fNodeRef.node = node;

        TRACE(("start session %ld:%lld \"%s\", system_time: %lld, active until: %lld\n",
                device, node, Name(), system_time(), fActiveUntil));
}


Session::Session(const char *name)
        :
        fNodeHash(NULL),
        fNodes(NULL),
        fClosing(false),
        fIsWatchingTeam(false)
{
        fTeam = -1;
        fNodeRef.device = -1;
        fNodeRef.node = -1;

        if (isdigit(name[0]))
                parse_node_ref(name, fNodeRef);

        strlcpy(fName, name, B_OS_NAME_LENGTH);
}


Session::~Session()
{
        mutex_destroy(&fLock);

        // free all nodes
        struct node *node, *next = NULL;

        if (fNodeHash) {
                // ... from the hash
                node = fNodeHash->Clear(true);
        } else {
                // ... from the list
                node = fNodes;
        }

        for (; node != NULL; node = next) {
                next = node->next;
                free(node);
        }

        delete fNodeHash;
        StopWatchingTeam();
}


status_t
Session::InitCheck()
{
        if (fNodeHash == NULL)
                return B_NO_MEMORY;

        return B_OK;
}


node *
Session::_FindNode(dev_t device, ino_t node)
{
        node_ref key;
        key.device = device;
        key.node = node;

        return fNodeHash->Lookup(key);
}


void
Session::AddNode(dev_t device, ino_t id)
{
        struct node *node = _FindNode(device, id);
        if (node != NULL) {
                node->ref_count++;
                return;
        }

        node = new_node(device, id);
        if (node == NULL)
                return;

        fNodeHash->Insert(node);
        fNodeCount++;
}


void
Session::RemoveNode(dev_t device, ino_t id)
{
        struct node *node = _FindNode(device, id);
        if (node != NULL && --node->ref_count <= 0) {
                fNodeHash->Remove(node);
                fNodeCount--;
        }
}


status_t
Session::StartWatchingTeam()
{
        if (Team() < B_OK)
                return B_OK;

        status_t status = start_watching_team(Team(), team_gone, this);
        if (status == B_OK)
                fIsWatchingTeam = true;

        return status;
}


void
Session::StopWatchingTeam()
{
        if (fIsWatchingTeam)
                stop_watching_team(Team(), team_gone, this);
}


void
Session::Prefetch()
{
        if (fNodes == NULL || fNodeHash != NULL)
                return;

        for (struct node *node = fNodes; node != NULL; node = node->next) {
                cache_prefetch(node->ref.device, node->ref.node, 0, ~0UL);
        }
}


status_t
Session::LoadFromDirectory(int directoryFD)
{
        TRACE(("load session %s\n", Name()));

        int fd = _kern_open(directoryFD, Name(), O_RDONLY, 0);
        if (fd < B_OK)
                return fd;

        struct stat stat;
        if (fstat(fd, &stat) != 0) {
                close(fd);
                return errno;
        }

        if (stat.st_size > 32768) {
                // for safety reasons
                // ToDo: make a bit larger later
                close(fd);
                return B_BAD_DATA;
        }

        char *buffer = (char *)malloc(stat.st_size);
        if (buffer == NULL) {
                close(fd);
                return B_NO_MEMORY;
        }

        if (read(fd, buffer, stat.st_size) < stat.st_size) {
                free(buffer);
                close(fd);
                return B_ERROR;
        }

        const char *line = buffer;
        node_ref nodeRef;
        while (parse_node_ref(line, nodeRef, &line)) {
                struct node *node = new_node(nodeRef.device, nodeRef.node);
                if (node != NULL) {
                        // note: this reverses the order of the nodes in the file
                        node->next = fNodes;
                        fNodes = node;
                }
                line++;
        }

        free(buffer);
        close(fd);
        return B_OK;
}


status_t
Session::Save()
{
        fClosing = true;

        char name[B_OS_NAME_LENGTH + 25];
        if (!IsMainSession()) {
                snprintf(name, sizeof(name), "/etc/launch_cache/%ld:%lld %s",
                        fNodeRef.device, fNodeRef.node, Name());
        } else
                snprintf(name, sizeof(name), "/etc/launch_cache/%s", Name());

        int fd = open(name, O_CREAT | O_TRUNC | O_WRONLY, 0644);
        if (fd < B_OK)
                return errno;

        status_t status = B_OK;
        off_t fileSize = 0;

        // ToDo: order nodes by timestamp... (should improve launch speed)
        // ToDo: test which parts of a file have been read (and save that as well)

        // enlarge file, so that it can be written faster
        ftruncate(fd, 512 * 1024);

        NodeTable::Iterator iterator(fNodeHash);
        while (iterator.HasNext()) {
                struct node *node = iterator.Next();
                snprintf(name, sizeof(name), "%ld:%lld\n", node->ref.device, node->ref.node);

                ssize_t bytesWritten = write(fd, name, strlen(name));
                if (bytesWritten < B_OK) {
                        status = bytesWritten;
                        break;
                }

                fileSize += bytesWritten;
        }

        ftruncate(fd, fileSize);
        close(fd);

        return status;
}


bool
Session::IsWorthSaving() const
{
        // ToDo: sort out entries with only very few nodes, and those that load
        //      instantly, anyway
        if (fNodeCount < 5 || system_time() - fTimestamp < 400000) {
                // sort anything out that opens less than 5 files, or needs less
                // than 0.4 seconds to load an run
                return false;
        }
        return true;
}


bool
Session::IsMainSession() const
{
        return fNodeRef.node == -1;
}


//      #pragma mark -


SessionGetter::SessionGetter(team_id team, Session **_session)
{
        RecursiveLocker locker(&sLock);

        if (sMainSession != NULL)
                fSession = sMainSession;
        else
                fSession = sTeamHash->Lookup(team);

        if (fSession != NULL) {
                if (!fSession->IsClosing())
                        fSession->Lock();
                else
                        fSession = NULL;
        }

        *_session = fSession;
}


SessionGetter::~SessionGetter()
{
        if (fSession != NULL)
                fSession->Unlock();
}


status_t
SessionGetter::New(const char *name, dev_t device, ino_t node,
        Session **_session)
{
        Thread *thread = thread_get_current_thread();
        fSession = start_session(thread->team->id, device, node, name);

        if (fSession != NULL) {
                *_session = fSession;
                return B_OK;
        }

        return B_ERROR;
}


void
SessionGetter::Stop()
{
        if (fSession == sMainSession)
                sMainSession = NULL;

        stop_session(fSession);
        fSession = NULL;
}

//      #pragma mark -


static void
node_opened(struct vnode *vnode, dev_t device, ino_t parent,
        ino_t node, const char *name, off_t size)
{
        if (device < gBootDevice) {
                // we ignore any access to rootfs, pipefs, and devfs
                // ToDo: if we can ever move the boot device on the fly, this will break
                return;
        }

        Session *session;
        SessionGetter getter(team_get_current_team_id(), &session);

        if (session == NULL) {
                char buffer[B_FILE_NAME_LENGTH];
                if (name == NULL
                        && vfs_get_vnode_name(vnode, buffer, sizeof(buffer)) == B_OK)
                        name = buffer;

                // create new session for this team
                getter.New(name, device, node, &session);
        }

        if (session == NULL || !session->IsActive()) {
                if (sMainSession != NULL) {
                        // ToDo: this opens a race condition with the "stop session" syscall
                        getter.Stop();
                }
                return;
        }

        session->AddNode(device, node);
}


static void
node_closed(struct vnode *vnode, dev_t device, ino_t node,
        int32 accessType)
{
        Session *session;
        SessionGetter getter(team_get_current_team_id(), &session);

        if (session == NULL)
                return;

        if (accessType == FILE_CACHE_NO_IO)
                session->RemoveNode(device, node);
}


static status_t
launch_speedup_control(const char *subsystem, uint32 function,
        void *buffer, size_t bufferSize)
{
        switch (function) {
                case LAUNCH_SPEEDUP_START_SESSION:
                {
                        char name[B_OS_NAME_LENGTH];
                        if (!IS_USER_ADDRESS(buffer)
                                || user_strlcpy(name, (const char *)buffer, B_OS_NAME_LENGTH) < B_OK)
                                return B_BAD_ADDRESS;

                        if (isdigit(name[0]) || name[0] == '.')
                                return B_BAD_VALUE;

                        sMainSession = start_session(-1, -1, -1, name, 60);
                        sMainSession->Unlock();
                        return B_OK;
                }

                case LAUNCH_SPEEDUP_STOP_SESSION:
                {
                        char name[B_OS_NAME_LENGTH];
                        if (!IS_USER_ADDRESS(buffer)
                                || user_strlcpy(name, (const char *)buffer, B_OS_NAME_LENGTH) < B_OK)
                                return B_BAD_ADDRESS;

                        // ToDo: this check is not thread-safe
                        if (sMainSession == NULL || strcmp(sMainSession->Name(), name))
                                return B_BAD_VALUE;

                        if (!strcmp(name, "system boot"))
                                dprintf("STOP BOOT %lld\n", system_time());

                        sMainSession->Lock();
                        stop_session(sMainSession);
                        sMainSession = NULL;
                        return B_OK;
                }
        }

        return B_BAD_VALUE;
}


static void
uninit()
{
        unregister_generic_syscall(LAUNCH_SPEEDUP_SYSCALLS, 1);

        recursive_lock_lock(&sLock);

        // free all sessions from the hashes

        Session *session = sTeamHash->Clear(true);
        while (session != NULL) {
                Session *next = session->next;
                delete session;
                session = next;
        }
        session = sPrefetchHash->Clear(true);
        while (session != NULL) {
                Session *next = session->next;
                delete session;
                session = next;
        }

        // free all sessions from the main prefetch list

        for (session = sMainPrefetchSessions; session != NULL; ) {
                sMainPrefetchSessions = session->Next();
                delete session;
                session = sMainPrefetchSessions;
        }

        delete sTeamHash;
        delete sPrefetchHash;
        recursive_lock_destroy(&sLock);
}


static status_t
init()
{
        sTeamHash = new(std::nothrow) SessionTable();
        if (sTeamHash == NULL || sTeamHash->Init(64) != B_OK)
                return B_NO_MEMORY;

        status_t status;

        sPrefetchHash = new(std::nothrow) PrefetchTable();
        if (sPrefetchHash == NULL || sPrefetchHash->Init(64) != B_OK) {
                status = B_NO_MEMORY;
                goto err1;
        }

        recursive_lock_init(&sLock, "launch speedup");

        // register kernel syscalls
        if (register_generic_syscall(LAUNCH_SPEEDUP_SYSCALLS,
                        launch_speedup_control, 1, 0) != B_OK) {
                status = B_ERROR;
                goto err3;
        }

        // read in prefetch knowledge base

        mkdir("/etc/launch_cache", 0755);
        load_prefetch_data();

        // start boot session

        sMainSession = start_session(-1, -1, -1, "system boot");
        sMainSession->Unlock();
        dprintf("START BOOT %lld\n", system_time());
        return B_OK;

err3:
        recursive_lock_destroy(&sLock);
        delete sPrefetchHash;
err1:
        delete sTeamHash;
        return status;
}


static status_t
std_ops(int32 op, ...)
{
        switch (op) {
                case B_MODULE_INIT:
                        return init();

                case B_MODULE_UNINIT:
                        uninit();
                        return B_OK;

                default:
                        return B_ERROR;
        }
}


static struct cache_module_info sLaunchSpeedupModule = {
        {
                CACHE_MODULES_NAME "/launch_speedup/v1",
                0,
                std_ops,
        },
        node_opened,
        node_closed,
        NULL,
};


module_info *modules[] = {
        (module_info *)&sLaunchSpeedupModule,
        NULL
};