diff options
-rw-r--r-- | doc/user.rst | 33 | ||||
-rw-r--r-- | src/Instance.cxx | 3 | ||||
-rw-r--r-- | src/Instance.hxx | 5 | ||||
-rw-r--r-- | src/Main.cxx | 9 | ||||
-rw-r--r-- | src/Partition.cxx | 50 | ||||
-rw-r--r-- | src/Partition.hxx | 8 | ||||
-rw-r--r-- | src/config/Option.hxx | 1 | ||||
-rw-r--r-- | src/config/Templates.cxx | 1 | ||||
-rw-r--r-- | src/decoder/Bridge.cxx | 14 | ||||
-rw-r--r-- | src/decoder/Bridge.hxx | 2 | ||||
-rw-r--r-- | src/decoder/Control.cxx | 2 | ||||
-rw-r--r-- | src/decoder/Control.hxx | 4 | ||||
-rw-r--r-- | src/decoder/Thread.cxx | 2 | ||||
-rw-r--r-- | src/input/BufferingInputStream.cxx | 12 | ||||
-rw-r--r-- | src/input/cache/Config.cxx | 35 | ||||
-rw-r--r-- | src/input/cache/Config.hxx | 33 | ||||
-rw-r--r-- | src/input/cache/Item.cxx | 62 | ||||
-rw-r--r-- | src/input/cache/Item.hxx | 80 | ||||
-rw-r--r-- | src/input/cache/Lease.hxx | 94 | ||||
-rw-r--r-- | src/input/cache/Manager.cxx | 163 | ||||
-rw-r--r-- | src/input/cache/Manager.hxx | 114 | ||||
-rw-r--r-- | src/input/cache/Stream.cxx | 96 | ||||
-rw-r--r-- | src/input/cache/Stream.hxx | 52 | ||||
-rw-r--r-- | src/input/meson.build | 4 | ||||
-rw-r--r-- | src/player/Control.cxx | 2 | ||||
-rw-r--r-- | src/player/Control.hxx | 4 | ||||
-rw-r--r-- | src/player/Thread.cxx | 1 |
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(); |