summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/user.rst33
-rw-r--r--src/Instance.cxx3
-rw-r--r--src/Instance.hxx5
-rw-r--r--src/Main.cxx9
-rw-r--r--src/Partition.cxx50
-rw-r--r--src/Partition.hxx8
-rw-r--r--src/config/Option.hxx1
-rw-r--r--src/config/Templates.cxx1
-rw-r--r--src/decoder/Bridge.cxx14
-rw-r--r--src/decoder/Bridge.hxx2
-rw-r--r--src/decoder/Control.cxx2
-rw-r--r--src/decoder/Control.hxx4
-rw-r--r--src/decoder/Thread.cxx2
-rw-r--r--src/input/BufferingInputStream.cxx12
-rw-r--r--src/input/cache/Config.cxx35
-rw-r--r--src/input/cache/Config.hxx33
-rw-r--r--src/input/cache/Item.cxx62
-rw-r--r--src/input/cache/Item.hxx80
-rw-r--r--src/input/cache/Lease.hxx94
-rw-r--r--src/input/cache/Manager.cxx163
-rw-r--r--src/input/cache/Manager.hxx114
-rw-r--r--src/input/cache/Stream.cxx96
-rw-r--r--src/input/cache/Stream.hxx52
-rw-r--r--src/input/meson.build4
-rw-r--r--src/player/Control.cxx2
-rw-r--r--src/player/Control.hxx4
-rw-r--r--src/player/Thread.cxx1
27 files changed, 880 insertions, 6 deletions
diff --git a/doc/user.rst b/doc/user.rst
index b129ad308..6f07cf89e 100644
--- a/doc/user.rst
+++ b/doc/user.rst
@@ -334,6 +334,39 @@ The following table lists the input options valid for all plugins:
More information can be found in the :ref:`input_plugins` reference.
+Configuring the Input Cache
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The input cache prefetches queued song files before they are going to
+be played. This has several advantages:
+
+- risk of buffer underruns during playback is reduced because this
+ decouples playback from disk (or network) I/O
+- bulk transfers may be faster and more energy efficient than loading
+ small chunks on-the-fly
+- by prefetching several songs at a time, the hard disk can spin down
+ for longer periods of time
+
+This comes at a cost:
+
+- memory usage
+- bulk transfers may reduce the performance of other applications
+ which also want to access the disk (if the kernel's I/O scheduler
+ isn't doing its job properly)
+
+To enable the input cache, add an ``input_cache`` block to the
+configuration file:
+
+.. code-block:: none
+
+ input_cache {
+ size "1 GB"
+ }
+
+This allocates a cache of 1 GB. If the cache grows larger than that,
+older files will be evicted.
+
+
Configuring decoder plugins
---------------------------
diff --git a/src/Instance.cxx b/src/Instance.cxx
index 090ae2295..7510a2781 100644
--- a/src/Instance.cxx
+++ b/src/Instance.cxx
@@ -1,5 +1,5 @@
/*
- * Copyright 2003-2018 The Music Player Daemon Project
+ * Copyright 2003-2019 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
@@ -23,6 +23,7 @@
#include "Idle.hxx"
#include "Stats.hxx"
#include "client/List.hxx"
+#include "input/cache/Manager.hxx"
#ifdef ENABLE_CURL
#include "RemoteTagCache.hxx"
diff --git a/src/Instance.hxx b/src/Instance.hxx
index 754524bef..294d44680 100644
--- a/src/Instance.hxx
+++ b/src/Instance.hxx
@@ -1,5 +1,5 @@
/*
- * Copyright 2003-2018 The Music Player Daemon Project
+ * Copyright 2003-2019 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
@@ -54,6 +54,7 @@ struct Partition;
class StateFile;
class RemoteTagCache;
class StickerDatabase;
+class InputCacheManager;
/**
* A utility class which, when used as the first base class, ensures
@@ -98,6 +99,8 @@ struct Instance final
Systemd::Watchdog systemd_watchdog;
#endif
+ std::unique_ptr<InputCacheManager> input_cache;
+
MaskMonitor idle_monitor;
#ifdef ENABLE_NEIGHBOR_PLUGINS
diff --git a/src/Main.cxx b/src/Main.cxx
index 86a10f3d2..52d81eb32 100644
--- a/src/Main.cxx
+++ b/src/Main.cxx
@@ -38,6 +38,8 @@
#include "Log.hxx"
#include "LogInit.hxx"
#include "input/Init.hxx"
+#include "input/cache/Config.hxx"
+#include "input/cache/Manager.hxx"
#include "event/Loop.hxx"
#include "fs/AllocatedPath.hxx"
#include "fs/Config.hxx"
@@ -414,6 +416,12 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
raw_config.GetPositive(ConfigOption::MAX_CONN, 10);
instance.client_list = std::make_unique<ClientList>(max_clients);
+ const auto *input_cache_config = raw_config.GetBlock(ConfigBlockOption::INPUT_CACHE);
+ if (input_cache_config != nullptr) {
+ const InputCacheConfig c(*input_cache_config);
+ instance.input_cache = std::make_unique<InputCacheManager>(c);
+ }
+
initialize_decoder_and_player(instance,
raw_config, config.replay_gain);
@@ -461,6 +469,7 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
client_manager_init(raw_config);
const ScopeInputPluginsInit input_plugins_init(raw_config,
instance.io_thread.GetEventLoop());
+
const ScopePlaylistPluginsInit playlist_plugins_init(raw_config);
#ifdef ENABLE_DAEMON
diff --git a/src/Partition.cxx b/src/Partition.cxx
index b2dd6bd0b..87a7f92d4 100644
--- a/src/Partition.cxx
+++ b/src/Partition.cxx
@@ -20,10 +20,15 @@
#include "config.h"
#include "Partition.hxx"
#include "Instance.hxx"
+#include "Log.hxx"
#include "song/DetachedSong.hxx"
#include "mixer/Volume.hxx"
#include "IdleFlags.hxx"
#include "client/Listener.hxx"
+#include "input/cache/Manager.hxx"
+#include "util/Domain.hxx"
+
+static constexpr Domain cache_domain("cache");
Partition::Partition(Instance &_instance,
const char *_name,
@@ -37,7 +42,9 @@ Partition::Partition(Instance &_instance,
global_events(instance.event_loop, BIND_THIS_METHOD(OnGlobalEvent)),
playlist(max_length, *this),
outputs(*this),
- pc(*this, outputs, buffer_chunks,
+ pc(*this, outputs,
+ instance.input_cache.get(),
+ buffer_chunks,
configured_audio_format, replay_gain_config)
{
UpdateEffectiveReplayGainMode();
@@ -51,6 +58,43 @@ Partition::EmitIdle(unsigned mask) noexcept
instance.EmitIdle(mask);
}
+static void
+PrefetchSong(InputCacheManager &cache, const char *uri) noexcept
+{
+ if (cache.Contains(uri))
+ return;
+
+ FormatDebug(cache_domain, "Prefetch '%s'", uri);
+
+ try {
+ cache.Prefetch(uri);
+ } catch (...) {
+ FormatError(std::current_exception(),
+ "Prefetch '%s' failed", uri);
+ }
+}
+
+static void
+PrefetchSong(InputCacheManager &cache, const DetachedSong &song) noexcept
+{
+ PrefetchSong(cache, song.GetURI());
+}
+
+inline void
+Partition::PrefetchQueue() noexcept
+{
+ if (!instance.input_cache)
+ return;
+
+ auto &cache = *instance.input_cache;
+
+ int next = playlist.GetNextPosition();
+ if (next >= 0)
+ PrefetchSong(cache, playlist.queue.Get(next));
+
+ // TODO: prefetch more songs
+}
+
void
Partition::UpdateEffectiveReplayGainMode() noexcept
{
@@ -106,6 +150,10 @@ void
Partition::SyncWithPlayer() noexcept
{
playlist.SyncWithPlayer(pc);
+
+ /* TODO: invoke this function in batches, to let the hard disk
+ spin down in between */
+ PrefetchQueue();
}
void
diff --git a/src/Partition.hxx b/src/Partition.hxx
index b4fd83af9..63f23d258 100644
--- a/src/Partition.hxx
+++ b/src/Partition.hxx
@@ -81,6 +81,14 @@ struct Partition final : QueueListener, PlayerListener, MixerListener {
void EmitIdle(unsigned mask) noexcept;
+ /**
+ * Populate the #InputCacheManager with soon-to-be-played song
+ * files.
+ *
+ * Errors will be logged.
+ */
+ void PrefetchQueue() noexcept;
+
void ClearQueue() noexcept {
playlist.Clear(pc);
}
diff --git a/src/config/Option.hxx b/src/config/Option.hxx
index 03da8e378..b5d6adfb7 100644
--- a/src/config/Option.hxx
+++ b/src/config/Option.hxx
@@ -86,6 +86,7 @@ enum class ConfigBlockOption {
AUDIO_OUTPUT,
DECODER,
INPUT,
+ INPUT_CACHE,
PLAYLIST_PLUGIN,
RESAMPLER,
AUDIO_FILTER,
diff --git a/src/config/Templates.cxx b/src/config/Templates.cxx
index 4da8031d4..36d3a2c5d 100644
--- a/src/config/Templates.cxx
+++ b/src/config/Templates.cxx
@@ -86,6 +86,7 @@ const ConfigTemplate config_block_templates[] = {
{ "audio_output", true },
{ "decoder", true },
{ "input", true },
+ { "input_cache" },
{ "playlist_plugin", true },
{ "resampler" },
{ "filter", true },
diff --git a/src/decoder/Bridge.cxx b/src/decoder/Bridge.cxx
index 36f991529..36c269489 100644
--- a/src/decoder/Bridge.cxx
+++ b/src/decoder/Bridge.cxx
@@ -31,6 +31,8 @@
#include "Log.hxx"
#include "input/InputStream.hxx"
#include "input/LocalOpen.hxx"
+#include "input/cache/Manager.hxx"
+#include "input/cache/Stream.hxx"
#include "fs/Path.hxx"
#include "util/ConstBuffer.hxx"
#include "util/StringBuffer.hxx"
@@ -52,8 +54,18 @@ DecoderBridge::~DecoderBridge() noexcept
}
InputStreamPtr
-DecoderBridge::OpenLocal(Path path_fs)
+DecoderBridge::OpenLocal(Path path_fs, const char *uri_utf8)
{
+ if (dc.input_cache != nullptr) {
+ auto lease = dc.input_cache->Get(uri_utf8, true);
+ if (lease) {
+ auto is = std::make_unique<CacheInputStream>(std::move(lease),
+ dc.mutex);
+ is->SetHandler(&dc);
+ return is;
+ }
+ }
+
return OpenLocalInputStream(path_fs, dc.mutex);
}
diff --git a/src/decoder/Bridge.hxx b/src/decoder/Bridge.hxx
index 2f1833b6d..3814d7a42 100644
--- a/src/decoder/Bridge.hxx
+++ b/src/decoder/Bridge.hxx
@@ -157,7 +157,7 @@ public:
/**
* Open a local file.
*/
- InputStreamPtr OpenLocal(Path path_fs);
+ InputStreamPtr OpenLocal(Path path_fs, const char *uri_utf8);
/* virtual methods from DecoderClient */
void Ready(AudioFormat audio_format,
diff --git a/src/decoder/Control.cxx b/src/decoder/Control.cxx
index 75473d5e2..c8c04c079 100644
--- a/src/decoder/Control.cxx
+++ b/src/decoder/Control.cxx
@@ -26,9 +26,11 @@
#include <assert.h>
DecoderControl::DecoderControl(Mutex &_mutex, Cond &_client_cond,
+ InputCacheManager *_input_cache,
const AudioFormat _configured_audio_format,
const ReplayGainConfig &_replay_gain_config) noexcept
:thread(BIND_THIS_METHOD(RunThread)),
+ input_cache(_input_cache),
mutex(_mutex), client_cond(_client_cond),
configured_audio_format(_configured_audio_format),
replay_gain_config(_replay_gain_config) {}
diff --git a/src/decoder/Control.hxx b/src/decoder/Control.hxx
index f6a45a2b4..e68cd609e 100644
--- a/src/decoder/Control.hxx
+++ b/src/decoder/Control.hxx
@@ -46,6 +46,7 @@
class DetachedSong;
class MusicBuffer;
class MusicPipe;
+class InputCacheManager;
enum class DecoderState : uint8_t {
STOP = 0,
@@ -68,6 +69,8 @@ class DecoderControl final : public InputStreamHandler {
Thread thread;
public:
+ InputCacheManager *const input_cache;
+
/**
* This lock protects #state and #command.
*
@@ -181,6 +184,7 @@ public:
* @param _client_cond see #client_cond
*/
DecoderControl(Mutex &_mutex, Cond &_client_cond,
+ InputCacheManager *_input_cache,
const AudioFormat _configured_audio_format,
const ReplayGainConfig &_replay_gain_config) noexcept;
~DecoderControl() noexcept;
diff --git a/src/decoder/Thread.cxx b/src/decoder/Thread.cxx
index 6bc1836ec..ec8cf49ad 100644
--- a/src/decoder/Thread.cxx
+++ b/src/decoder/Thread.cxx
@@ -346,7 +346,7 @@ decoder_run_file(DecoderBridge &bridge, const char *uri_utf8, Path path_fs)
InputStreamPtr input_stream;
try {
- input_stream = bridge.OpenLocal(path_fs);
+ input_stream = bridge.OpenLocal(path_fs, uri_utf8);
} catch (const std::system_error &e) {
if (IsPathNotFound(e) &&
/* ENOTDIR means this may be a path inside a
diff --git a/src/input/BufferingInputStream.cxx b/src/input/BufferingInputStream.cxx
index 8888f597c..9bdb91b49 100644
--- a/src/input/BufferingInputStream.cxx
+++ b/src/input/BufferingInputStream.cxx
@@ -152,6 +152,18 @@ BufferingInputStream::RunThreadLocked(std::unique_lock<Mutex> &lock)
continue;
}
+ /* enforce an upper limit for each
+ InputStream::Read() call; this is necessary
+ for plugins which are unable to do partial
+ reads, e.g. when reading local files, the
+ read() system call will not return until
+ all requested bytes have been read from the
+ hard disk, instead of returning when "some"
+ data has been read */
+ constexpr size_t MAX_READ = 64 * 1024;
+ if (w.size > MAX_READ)
+ w.size = MAX_READ;
+
size_t nbytes = input->Read(lock, w.data, w.size);
buffer.Commit(read_offset, read_offset + nbytes);
diff --git a/src/input/cache/Config.cxx b/src/input/cache/Config.cxx
new file mode 100644
index 000000000..efb472497
--- /dev/null
+++ b/src/input/cache/Config.cxx
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2003-2019 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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 of the License, 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "Config.hxx"
+#include "config/Block.hxx"
+#include "config/Parser.hxx"
+
+static constexpr size_t KILOBYTE = 1024;
+static constexpr size_t MEGABYTE = 1024 * KILOBYTE;
+
+InputCacheConfig::InputCacheConfig(const ConfigBlock &block)
+{
+ size = 256 * MEGABYTE;
+ const auto *size_param = block.GetBlockParam("size");
+ if (size_param != nullptr)
+ size = size_param->With([](const char *s){
+ return ParseSize(s);
+ });
+}
diff --git a/src/input/cache/Config.hxx b/src/input/cache/Config.hxx
new file mode 100644
index 000000000..3ea1c551c
--- /dev/null
+++ b/src/input/cache/Config.hxx
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2003-2019 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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 of the License, 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_INPUT_CACHE_CONFIG_HXX
+#define MPD_INPUT_CACHE_CONFIG_HXX
+
+#include <stddef.h>
+
+struct ConfigBlock;
+
+struct InputCacheConfig {
+ size_t size;
+
+ explicit InputCacheConfig(const ConfigBlock &block);
+};
+
+#endif
diff --git a/src/input/cache/Item.cxx b/src/input/cache/Item.cxx
new file mode 100644
index 000000000..5decaaced
--- /dev/null
+++ b/src/input/cache/Item.cxx
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2003-2019 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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 of the License, 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "Item.hxx"
+#include "Lease.hxx"
+
+#include <assert.h>
+
+InputCacheItem::InputCacheItem(InputStreamPtr _input) noexcept
+ :BufferingInputStream(std::move(_input)),
+ uri(GetInput().GetURI())
+{
+}
+
+InputCacheItem::~InputCacheItem() noexcept
+{
+ assert(leases.empty());
+}
+
+void
+InputCacheItem::AddLease(InputCacheLease &lease) noexcept
+{
+ const std::lock_guard<Mutex> lock(mutex);
+ leases.push_back(lease);
+}
+
+void
+InputCacheItem::RemoveLease(InputCacheLease &lease) noexcept
+{
+ const std::lock_guard<Mutex> lock(mutex);
+ auto i = leases.iterator_to(lease);
+ if (i == next_lease)
+ ++next_lease;
+ leases.erase(i);
+
+ // TODO: ensure that OnBufferAvailable() isn't currently running
+}
+
+void
+InputCacheItem::OnBufferAvailable() noexcept
+{
+ for (auto i = leases.begin(); i != leases.end(); i = next_lease) {
+ next_lease = std::next(i);
+ i->OnInputCacheAvailable();
+ }
+}
diff --git a/src/input/cache/Item.hxx b/src/input/cache/Item.hxx
new file mode 100644
index 000000000..53b9668a1
--- /dev/null
+++ b/src/input/cache/Item.hxx
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2003-2019 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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 of the License, 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_INPUT_CACHE_ITEM_HXX
+#define MPD_INPUT_CACHE_ITEM_HXX
+
+#include "input/BufferingInputStream.hxx"
+#include "thread/Mutex.hxx"
+#include "util/SparseBuffer.hxx"
+
+#include <boost/intrusive/list.hpp>
+#include <boost/intrusive/set_hook.hpp>
+
+#include <memory>
+#include <string>
+
+class InputCacheLease;
+
+/**
+ * An item in the #InputCacheManager. It caches the contents of a
+ * file, and reading and managing it through the base class
+ * #BufferingInputStream.
+ *
+ * Use the class #CacheInputStream to read from it.
+ */
+class InputCacheItem final
+ : public BufferingInputStream,
+ public boost::intrusive::list_base_hook<boost::intrusive::link_mode<boost::intrusive::auto_unlink>>,
+ public boost::intrusive::set_base_hook<boost::intrusive::link_mode<boost::intrusive::normal_link>>
+{
+ const std::string uri;
+
+ using LeaseList =
+ boost::intrusive::list<InputCacheLease,
+ boost::intrusive::base_hook<boost::intrusive::list_base_hook<boost::intrusive::link_mode<boost::intrusive::normal_link>>>,
+ boost::intrusive::constant_time_size<false>>;
+
+ LeaseList leases;
+ LeaseList::iterator next_lease = leases.end();
+
+public:
+ explicit InputCacheItem(InputStreamPtr _input) noexcept;
+ ~InputCacheItem() noexcept;
+
+ const char *GetUri() const noexcept {
+ return uri.c_str();
+ }
+
+ using BufferingInputStream::size;
+
+ bool IsInUse() const noexcept {
+ const std::lock_guard<Mutex> lock(mutex);
+ return !leases.empty();
+ }
+
+ void AddLease(InputCacheLease &lease) noexcept;
+ void RemoveLease(InputCacheLease &lease) noexcept;
+
+private:
+ /* virtual methods from class BufferingInputStream */
+ void OnBufferAvailable() noexcept override;
+};
+
+#endif
diff --git a/src/input/cache/Lease.hxx b/src/input/cache/Lease.hxx
new file mode 100644
index 000000000..be0380e84
--- /dev/null
+++ b/src/input/cache/Lease.hxx
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2003-2019 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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 of the License, 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_INPUT_CACHE_LEASE_HXX
+#define MPD_INPUT_CACHE_LEASE_HXX
+
+#include "Item.hxx"
+
+#include <boost/intrusive/list_hook.hpp>
+
+#include <utility>
+
+/**
+ * A lease for an #InputCacheItem.
+ */
+class InputCacheLease
+ : public boost::intrusive::list_base_hook<boost::intrusive::link_mode<boost::intrusive::normal_link>>
+{
+ InputCacheItem *item = nullptr;
+
+public:
+ InputCacheLease() = default;
+
+ explicit InputCacheLease(InputCacheItem &_item) noexcept
+ :item(&_item)
+ {
+ item->AddLease(*this);
+ }
+
+ InputCacheLease(InputCacheLease &&src) noexcept
+ :item(std::exchange(src.item, nullptr))
+ {
+ if (item != nullptr) {
+ item->RemoveLease(src);
+ item->AddLease(*this);
+ }
+ }
+
+ ~InputCacheLease() noexcept {
+ if (item != nullptr)
+ item->RemoveLease(*this);
+ }
+
+ InputCacheLease &operator=(InputCacheLease &&src) noexcept {
+ using std::swap;
+ swap(item, src.item);
+
+ if (item != nullptr) {
+ item->RemoveLease(src);
+ item->AddLease(*this);
+ }
+
+ return *this;
+ }
+
+ operator bool() const noexcept {
+ return item != nullptr;
+ }
+
+ auto &operator*() const noexcept {
+ return *item;
+ }
+
+ auto *operator->() const noexcept {
+ return item;
+ }
+
+ auto &GetCacheItem() const noexcept {
+ return *item;
+ }
+
+ /**
+ * Caller locks #InputCacheItem::mutex.
+ */
+ virtual void OnInputCacheAvailable() noexcept {}
+};
+
+#endif
diff --git a/src/input/cache/Manager.cxx b/src/input/cache/Manager.cxx
new file mode 100644
index 000000000..3ff51d88e
--- /dev/null
+++ b/src/input/cache/Manager.cxx
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2003-2019 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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 of the License, 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "Manager.hxx"
+#include "Config.hxx"
+#include "Item.hxx"
+#include "Lease.hxx"
+#include "input/InputStream.hxx"
+#include "fs/Traits.hxx"
+#include "util/DeleteDisposer.hxx"
+
+#include <string.h>
+
+inline bool
+InputCacheManager::ItemCompare::operator()(const InputCacheItem &a,
+ const char *b) const noexcept
+{
+ return strcmp(a.GetUri(), b) < 0;
+}
+
+inline bool
+InputCacheManager::ItemCompare::operator()(const char *a,
+ const InputCacheItem &b) const noexcept
+{
+ return strcmp(a, b.GetUri()) < 0;
+}
+
+inline bool
+InputCacheManager::ItemCompare::operator()(const InputCacheItem &a,
+ const InputCacheItem &b) const noexcept
+{
+ return strcmp(a.GetUri(), b.GetUri()) < 0;
+}
+
+InputCacheManager::InputCacheManager(const InputCacheConfig &config) noexcept
+ :max_total_size(config.size)
+{
+}
+
+InputCacheManager::~InputCacheManager() noexcept
+{
+ items_by_time.clear_and_dispose(DeleteDisposer());
+}
+
+bool
+InputCacheManager::IsEligible(const InputStream &input) noexcept
+{
+ assert(input.IsReady());
+
+ return input.IsSeekable() && input.KnownSize() &&
+ input.GetSize() > 0 &&
+ input.GetSize() <= max_total_size / 2;
+}
+
+bool
+InputCacheManager::Contains(const char *uri) noexcept
+{
+ return Get(uri, false);
+}
+
+InputCacheLease
+InputCacheManager::Get(const char *uri, bool create)
+{
+ // TODO: allow caching remote files
+ if (!PathTraitsUTF8::IsAbsolute(uri))
+ return {};
+
+ UriMap::insert_commit_data hint;
+ auto result = items_by_uri.insert_check(uri, items_by_uri.key_comp(),
+ hint);
+ if (!result.second) {
+ auto &item = *result.first;
+
+ /* refresh */
+ items_by_time.erase(items_by_time.iterator_to(item));
+ items_by_time.push_back(item);
+
+ // TODO revalidate the cache item using the file's mtime?
+ // TODO if cache item contains error, retry now?
+
+ return InputCacheLease(item);
+ }
+
+ if (!create)
+ return {};
+
+ // TODO: wait for "ready" without blocking here
+ auto is = InputStream::OpenReady(uri, mutex);
+
+ if (!IsEligible(*is))
+ return {};
+
+ const size_t size = is->GetSize();
+ total_size += size;
+
+ while (total_size > max_total_size && EvictOldestUnused()) {}
+
+ auto *item = new InputCacheItem(std::move(is));
+ items_by_uri.insert_commit(*item, hint);
+ items_by_time.push_back(*item);
+
+ return InputCacheLease(*item);
+}
+
+void
+InputCacheManager::Prefetch(const char *uri)
+{
+ Get(uri, true);
+}
+
+void
+InputCacheManager::Remove(InputCacheItem &item) noexcept
+{
+ assert(total_size >= item.size());
+ total_size -= item.size();
+
+ items_by_time.erase(items_by_time.iterator_to(item));
+ items_by_uri.erase(items_by_uri.iterator_to(item));
+}
+
+void
+InputCacheManager::Delete(InputCacheItem *item) noexcept
+{
+ Remove(*item);
+ delete item;
+}
+
+InputCacheItem *
+InputCacheManager::FindOldestUnused() noexcept
+{
+ for (auto &i : items_by_time)
+ if (!i.IsInUse())
+ return &i;
+
+ return nullptr;
+}
+
+bool
+InputCacheManager::EvictOldestUnused() noexcept
+{
+ auto *item = FindOldestUnused();
+ if (item == nullptr)
+ return false;
+
+ Delete(item);
+ return true;
+}
diff --git a/src/input/cache/Manager.hxx b/src/input/cache/Manager.hxx
new file mode 100644
index 000000000..6fba1fbb3
--- /dev/null
+++ b/src/input/cache/Manager.hxx
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2003-2019 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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 of the License, 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_INPUT_CACHE_MANAGER_HXX
+#define MPD_INPUT_CACHE_MANAGER_HXX
+
+#include "input/Offset.hxx"
+#include "thread/Mutex.hxx"
+#include "util/Compiler.h"
+
+#include <boost/intrusive/set.hpp>
+#include <boost/intrusive/list.hpp>
+
+class InputStream;
+class InputCacheItem;
+class InputCacheLease;
+struct InputCacheConfig;
+
+/**
+ * A class which caches files in RAM. It is supposed to prefetch
+ * files before they are played.
+ */
+class InputCacheManager {
+ const size_t max_total_size;
+
+ mutable Mutex mutex;
+
+ size_t total_size = 0;
+
+ struct ItemCompare {
+ gcc_pure
+ bool operator()(const InputCacheItem &a,
+ const char *b) const noexcept;
+
+ gcc_pure
+ bool operator()(const char *a,
+ const InputCacheItem &b) const noexcept;
+
+ gcc_pure
+ bool operator()(const InputCacheItem &a,
+ const InputCacheItem &b) const noexcept;
+ };
+
+ boost::intrusive::list<InputCacheItem,
+ boost::intrusive::base_hook<boost::intrusive::list_base_hook<boost::intrusive::link_mode<boost::intrusive::auto_unlink>>>,
+ boost::intrusive::constant_time_size<false>> items_by_time;
+
+ using UriMap =
+ boost::intrusive::set<InputCacheItem,
+ boost::intrusive::base_hook<boost::intrusive::set_base_hook<boost::intrusive::link_mode<boost::intrusive::normal_link>>>,
+ boost::intrusive::compare<ItemCompare>,
+ boost::intrusive::constant_time_size<false>>;
+
+ UriMap items_by_uri;
+
+public:
+ explicit InputCacheManager(const InputCacheConfig &config) noexcept;
+ ~InputCacheManager() noexcept;
+
+ gcc_pure
+ bool Contains(const char *uri) noexcept;
+
+ /**
+ * Throws if opening the #InputStream fails.
+ *
+ * @param create if true, then the cache item will be created
+ * if it did not exist
+ * @return a lease of the new item or nullptr if the file is
+ * not eligible for caching
+ */
+ InputCacheLease Get(const char *uri, bool create);
+
+ /**
+ * Shortcut for "Get(uri,true)", discarding the returned
+ * lease.
+ */
+ void Prefetch(const char *uri);
+
+private:
+ /**
+ * Check whether the given #InputStream can be stored in this
+ * cache.
+ */
+ bool IsEligible(const InputStream &input) noexcept;
+
+ void Remove(InputCacheItem &item) noexcept;
+ void Delete(InputCacheItem *item) noexcept;
+
+ InputCacheItem *FindOldestUnused() noexcept;
+
+ /**
+ * @return true if one item has been evicted, false if no
+ * unused item was found
+ */
+ bool EvictOldestUnused() noexcept;
+};
+
+#endif
diff --git a/src/input/cache/Stream.cxx b/src/input/cache/Stream.cxx
new file mode 100644
index 000000000..5ce56eaef
--- /dev/null
+++ b/src/input/cache/Stream.cxx
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2003-2019 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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 of the License, 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "Stream.hxx"
+
+CacheInputStream::CacheInputStream(InputCacheLease _lease,
+ Mutex &_mutex) noexcept
+ :InputStream(_lease->GetUri(), _mutex),
+ InputCacheLease(std::move(_lease))
+{
+ auto &i = GetCacheItem();
+ size = i.size();
+ seekable = true;
+ SetReady();
+}
+
+void
+CacheInputStream::Check()
+{
+ const ScopeUnlock unlock(mutex);
+
+ auto &i = GetCacheItem();
+ const std::lock_guard<Mutex> protect(i.mutex);
+
+ i.Check();
+}
+
+void
+CacheInputStream::Seek(std::unique_lock<Mutex> &, offset_type new_offset)
+{
+ offset = new_offset;
+}
+
+bool
+CacheInputStream::IsEOF() const noexcept
+{
+ return offset == size;
+}
+
+bool
+CacheInputStream::IsAvailable() const noexcept
+{
+ const auto _offset = offset;
+ const ScopeUnlock unlock(mutex);
+
+ auto &i = GetCacheItem();
+ const std::lock_guard<Mutex> protect(i.mutex);
+
+ return i.IsAvailable(_offset);
+}
+
+size_t
+CacheInputStream::Read(std::unique_lock<Mutex> &lock,
+ void *ptr, size_t read_size)
+{
+ const auto _offset = offset;
+ auto &i = GetCacheItem();
+
+ size_t nbytes;
+
+ {
+ const ScopeUnlock unlock(mutex);
+ const std::lock_guard<Mutex> protect(i.mutex);
+
+ nbytes = i.Read(lock, _offset, ptr, read_size);
+ }
+
+ offset += nbytes;
+ return nbytes;
+}
+
+void
+CacheInputStream::OnInputCacheAvailable() noexcept
+{
+ auto &i = GetCacheItem();
+ const ScopeUnlock unlock(i.mutex);
+
+ const std::lock_guard<Mutex> protect(mutex);
+ InvokeOnAvailable();
+}
diff --git a/src/input/cache/Stream.hxx b/src/input/cache/Stream.hxx
new file mode 100644
index 000000000..ae039359c
--- /dev/null
+++ b/src/input/cache/Stream.hxx
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2003-2019 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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 of the License, 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_CACHE_INPUT_STREAM_HXX
+#define MPD_CACHE_INPUT_STREAM_HXX
+
+#include "Lease.hxx"
+#include "input/InputStream.hxx"
+
+/**
+ * An #InputStream implementation which reads data from an
+ * #InputCacheItem.
+ */
+class CacheInputStream final : public InputStream, InputCacheLease {
+public:
+ CacheInputStream(InputCacheLease _lease, Mutex &_mutex) noexcept;
+
+ /* virtual methods from class InputStream */
+ void Check() override;
+ /* we don't need to implement Update() because all attributes
+ have been copied already in our constructor */
+ //void Update() noexcept;
+ void Seek(std::unique_lock<Mutex> &lock, offset_type offset) override;
+ bool IsEOF() const noexcept override;
+ /* we don't support tags */
+ // std::unique_ptr<Tag> ReadTag() override;
+ bool IsAvailable() const noexcept override;
+ size_t Read(std::unique_lock<Mutex> &lock,
+ void *ptr, size_t size) override;
+
+private:
+ /* virtual methods from class InputCacheLease */
+ void OnInputCacheAvailable() noexcept override;
+};
+
+#endif
diff --git a/src/input/meson.build b/src/input/meson.build
index 3502379d8..5f7533751 100644
--- a/src/input/meson.build
+++ b/src/input/meson.build
@@ -35,6 +35,10 @@ input_glue = static_library(
'BufferingInputStream.cxx',
'BufferedInputStream.cxx',
'MaybeBufferedInputStream.cxx',
+ 'cache/Config.cxx',
+ 'cache/Manager.cxx',
+ 'cache/Item.cxx',
+ 'cache/Stream.cxx',
include_directories: inc,
)
diff --git a/src/player/Control.cxx b/src/player/Control.cxx
index 2ba4f29df..38f2288e9 100644
--- a/src/player/Control.cxx
+++ b/src/player/Control.cxx
@@ -28,10 +28,12 @@
PlayerControl::PlayerControl(PlayerListener &_listener,
PlayerOutputs &_outputs,
+ InputCacheManager *_input_cache,
unsigned _buffer_chunks,
AudioFormat _configured_audio_format,
const ReplayGainConfig &_replay_gain_config) noexcept
:listener(_listener), outputs(_outputs),
+ input_cache(_input_cache),
buffer_chunks(_buffer_chunks),
configured_audio_format(_configured_audio_format),
thread(BIND_THIS_METHOD(RunThread)),
diff --git a/src/player/Control.hxx b/src/player/Control.hxx
index 51861d6ec..159a5b7f3 100644
--- a/src/player/Control.hxx
+++ b/src/player/Control.hxx
@@ -39,6 +39,7 @@
struct Tag;
class PlayerListener;
class PlayerOutputs;
+class InputCacheManager;
class DetachedSong;
enum class PlayerState : uint8_t {
@@ -116,6 +117,8 @@ class PlayerControl final : public AudioOutputClient {
PlayerOutputs &outputs;
+ InputCacheManager *const input_cache;
+
const unsigned buffer_chunks;
/**
@@ -234,6 +237,7 @@ class PlayerControl final : public AudioOutputClient {
public:
PlayerControl(PlayerListener &_listener,
PlayerOutputs &_outputs,
+ InputCacheManager *_input_cache,
unsigned buffer_chunks,
AudioFormat _configured_audio_format,
const ReplayGainConfig &_replay_gain_config) noexcept;
diff --git a/src/player/Thread.cxx b/src/player/Thread.cxx
index d2293b17d..c07cfbfce 100644
--- a/src/player/Thread.cxx
+++ b/src/player/Thread.cxx
@@ -1132,6 +1132,7 @@ try {
SetThreadName("player");
DecoderControl dc(mutex, cond,
+ input_cache,
configured_audio_format,
replay_gain_config);
dc.StartThread();