content/media/MediaCache.h

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

     1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
     3 /* This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this
     5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     7 #ifndef MediaCache_h_
     8 #define MediaCache_h_
    10 #include "nsTArray.h"
    11 #include "nsCOMPtr.h"
    12 #include "nsHashKeys.h"
    13 #include "nsTHashtable.h"
    15 class nsIPrincipal;
    17 namespace mozilla {
    18 // defined in MediaResource.h
    19 class ChannelMediaResource;
    20 class MediaByteRange;
    21 class MediaResource;
    22 class ReentrantMonitorAutoEnter;
    24 /**
    25  * Media applications want fast, "on demand" random access to media data,
    26  * for pausing, seeking, etc. But we are primarily interested
    27  * in transporting media data using HTTP over the Internet, which has
    28  * high latency to open a connection, requires a new connection for every
    29  * seek, may not even support seeking on some connections (especially
    30  * live streams), and uses a push model --- data comes from the server
    31  * and you don't have much control over the rate. Also, transferring data
    32  * over the Internet can be slow and/or unpredictable, so we want to read
    33  * ahead to buffer and cache as much data as possible.
    34  * 
    35  * The job of the media cache is to resolve this impedance mismatch.
    36  * The media cache reads data from Necko channels into file-backed storage,
    37  * and offers a random-access file-like API to the stream data
    38  * (MediaCacheStream). Along the way it solves several problems:
    39  * -- The cache intelligently reads ahead to prefetch data that may be
    40  * needed in the future
    41  * -- The size of the cache is bounded so that we don't fill up
    42  * storage with read-ahead data
    43  * -- Cache replacement is managed globally so that the most valuable
    44  * data (across all streams) is retained
    45  * -- The cache can suspend Necko channels temporarily when their data is
    46  * not wanted (yet)
    47  * -- The cache translates file-like seek requests to HTTP seeks,
    48  * including optimizations like not triggering a new seek if it would
    49  * be faster to just keep reading until we reach the seek point. The
    50  * "seek to EOF" idiom to determine file size is also handled efficiently
    51  * (seeking to EOF and then seeking back to the previous offset does not
    52  * trigger any Necko activity)
    53  * -- The cache also handles the case where the server does not support
    54  * seeking
    55  * -- Necko can only send data to the main thread, but MediaCacheStream
    56  * can distribute data to any thread
    57  * -- The cache exposes APIs so clients can detect what data is
    58  * currently held
    59  * 
    60  * Note that although HTTP is the most important transport and we only
    61  * support transport-level seeking via HTTP byte-ranges, the media cache
    62  * works with any kind of Necko channels and provides random access to
    63  * cached data even for, e.g., FTP streams.
    64  * 
    65  * The media cache is not persistent. It does not currently allow
    66  * data from one load to be used by other loads, either within the same
    67  * browser session or across browser sessions. The media cache file
    68  * is marked "delete on close" so it will automatically disappear in the
    69  * event of a browser crash or shutdown.
    70  * 
    71  * The media cache is block-based. Streams are divided into blocks of a
    72  * fixed size (currently 4K) and we cache blocks. A single cache contains
    73  * blocks for all streams.
    74  * 
    75  * The cache size is controlled by the media.cache_size preference
    76  * (which is in KB). The default size is 500MB.
    77  * 
    78  * The replacement policy predicts a "time of next use" for each block
    79  * in the cache. When we need to free a block, the block with the latest
    80  * "time of next use" will be evicted. Blocks are divided into
    81  * different classes, each class having its own predictor:
    82  * FREE_BLOCK: these blocks are effectively infinitely far in the future;
    83  * a free block will always be chosen for replacement before other classes
    84  * of blocks.
    85  * METADATA_BLOCK: these are blocks that contain data that has been read
    86  * by the decoder in "metadata mode", e.g. while the decoder is searching
    87  * the stream during a seek operation. These blocks are managed with an
    88  * LRU policy; the "time of next use" is predicted to be as far in the
    89  * future as the last use was in the past.
    90  * PLAYED_BLOCK: these are blocks that have not been read in "metadata
    91  * mode", and contain data behind the current decoder read point. (They
    92  * may not actually have been read by the decoder, if the decoder seeked
    93  * forward.) These blocks are managed with an LRU policy except that we add
    94  * REPLAY_DELAY seconds of penalty to their predicted "time of next use",
    95  * to reflect the uncertainty about whether replay will actually happen
    96  * or not.
    97  * READAHEAD_BLOCK: these are blocks that have not been read in
    98  * "metadata mode" and that are entirely ahead of the current decoder
    99  * read point. (They may actually have been read by the decoder in the
   100  * past if the decoder has since seeked backward.) We predict the
   101  * time of next use for these blocks by assuming steady playback and
   102  * dividing the number of bytes between the block and the current decoder
   103  * read point by the decoder's estimate of its playback rate in bytes
   104  * per second. This ensures that the blocks farthest ahead are considered
   105  * least valuable.
   106  * For efficient prediction of the "latest time of next use", we maintain
   107  * linked lists of blocks in each class, ordering blocks by time of
   108  * next use. READAHEAD_BLOCKS have one linked list per stream, since their
   109  * time of next use depends on stream parameters, but the other lists
   110  * are global.
   111  * 
   112  * A block containing a current decoder read point can contain data
   113  * both behind and ahead of the read point. It will be classified as a
   114  * PLAYED_BLOCK but we will give it special treatment so it is never
   115  * evicted --- it actually contains the highest-priority readahead data
   116  * as well as played data.
   117  * 
   118  * "Time of next use" estimates are also used for flow control. When
   119  * reading ahead we can predict the time of next use for the data that
   120  * will be read. If the predicted time of next use is later then the
   121  * prediction for all currently cached blocks, and the cache is full, then
   122  * we should suspend reading from the Necko channel.
   123  * 
   124  * Unfortunately suspending the Necko channel can't immediately stop the
   125  * flow of data from the server. First our desire to suspend has to be
   126  * transmitted to the server (in practice, Necko stops reading from the
   127  * socket, which causes the kernel to shrink its advertised TCP receive
   128  * window size to zero). Then the server can stop sending the data, but
   129  * we will receive data roughly corresponding to the product of the link
   130  * bandwidth multiplied by the round-trip latency. We deal with this by
   131  * letting the cache overflow temporarily and then trimming it back by
   132  * moving overflowing blocks back into the body of the cache, replacing
   133  * less valuable blocks as they become available. We try to avoid simply
   134  * discarding overflowing readahead data.
   135  * 
   136  * All changes to the actual contents of the cache happen on the main
   137  * thread, since that's where Necko's notifications happen.
   138  *
   139  * The media cache maintains at most one Necko channel for each stream.
   140  * (In the future it might be advantageous to relax this, e.g. so that a
   141  * seek to near the end of the file can happen without disturbing
   142  * the loading of data from the beginning of the file.) The Necko channel
   143  * is managed through ChannelMediaResource; MediaCache does not
   144  * depend on Necko directly.
   145  * 
   146  * Every time something changes that might affect whether we want to
   147  * read from a Necko channel, or whether we want to seek on the Necko
   148  * channel --- such as data arriving or data being consumed by the
   149  * decoder --- we asynchronously trigger MediaCache::Update on the main
   150  * thread. That method implements most cache policy. It evaluates for
   151  * each stream whether we want to suspend or resume the stream and what
   152  * offset we should seek to, if any. It is also responsible for trimming
   153  * back the cache size to its desired limit by moving overflowing blocks
   154  * into the main part of the cache.
   155  * 
   156  * Streams can be opened in non-seekable mode. In non-seekable mode,
   157  * the cache will only call ChannelMediaResource::CacheClientSeek with
   158  * a 0 offset. The cache tries hard not to discard readahead data
   159  * for non-seekable streams, since that could trigger a potentially
   160  * disastrous re-read of the entire stream. It's up to cache clients
   161  * to try to avoid requesting seeks on such streams.
   162  * 
   163  * MediaCache has a single internal monitor for all synchronization.
   164  * This is treated as the lowest level monitor in the media code. So,
   165  * we must not acquire any MediaDecoder locks or MediaResource locks
   166  * while holding the MediaCache lock. But it's OK to hold those locks
   167  * and then get the MediaCache lock.
   168  * 
   169  * MediaCache associates a principal with each stream. CacheClientSeek
   170  * can trigger new HTTP requests; due to redirects to other domains,
   171  * each HTTP load can return data with a different principal. This
   172  * principal must be passed to NotifyDataReceived, and MediaCache
   173  * will detect when different principals are associated with data in the
   174  * same stream, and replace them with a null principal.
   175  */
   176 class MediaCache;
   178 /**
   179  * If the cache fails to initialize then Init will fail, so nonstatic
   180  * methods of this class can assume gMediaCache is non-null.
   181  * 
   182  * This class can be directly embedded as a value.
   183  */
   184 class MediaCacheStream {
   185 public:
   186   enum {
   187     // This needs to be a power of two
   188     BLOCK_SIZE = 32768
   189   };
   190   enum ReadMode {
   191     MODE_METADATA,
   192     MODE_PLAYBACK
   193   };
   195   // aClient provides the underlying transport that cache will use to read
   196   // data for this stream.
   197   MediaCacheStream(ChannelMediaResource* aClient);
   198   ~MediaCacheStream();
   200   // Set up this stream with the cache. Can fail on OOM. One
   201   // of InitAsClone or Init must be called before any other method on
   202   // this class. Does nothing if already initialized.
   203   nsresult Init();
   205   // Set up this stream with the cache, assuming it's for the same data
   206   // as the aOriginal stream. Can fail on OOM. Exactly one
   207   // of InitAsClone or Init must be called before any other method on
   208   // this class. Does nothing if already initialized.
   209   nsresult InitAsClone(MediaCacheStream* aOriginal);
   211   // These are called on the main thread.
   212   // Tell us whether the stream is seekable or not. Non-seekable streams
   213   // will always pass 0 for aOffset to CacheClientSeek. This should only
   214   // be called while the stream is at channel offset 0. Seekability can
   215   // change during the lifetime of the MediaCacheStream --- every time
   216   // we do an HTTP load the seekability may be different (and sometimes
   217   // is, in practice, due to the effects of caching proxies).
   218   void SetTransportSeekable(bool aIsTransportSeekable);
   219   // This must be called (and return) before the ChannelMediaResource
   220   // used to create this MediaCacheStream is deleted.
   221   void Close();
   222   // This returns true when the stream has been closed
   223   bool IsClosed() const { return mClosed; }
   224   // Returns true when this stream is can be shared by a new resource load
   225   bool IsAvailableForSharing() const
   226   {
   227     return !mClosed &&
   228       (!mDidNotifyDataEnded || NS_SUCCEEDED(mNotifyDataEndedStatus));
   229   }
   230   // Get the principal for this stream. Anything accessing the contents of
   231   // this stream must have a principal that subsumes this principal.
   232   nsIPrincipal* GetCurrentPrincipal() { return mPrincipal; }
   233   // Ensure a global media cache update has run with this stream present.
   234   // This ensures the cache has had a chance to suspend or unsuspend this stream.
   235   // Called only on main thread. This can change the state of streams, fire
   236   // notifications, etc.
   237   void EnsureCacheUpdate();
   239   // These callbacks are called on the main thread by the client
   240   // when data has been received via the channel.
   241   // Tells the cache what the server said the data length is going to be.
   242   // The actual data length may be greater (we receive more data than
   243   // specified) or smaller (the stream ends before we reach the given
   244   // length), because servers can lie. The server's reported data length
   245   // *and* the actual data length can even vary over time because a
   246   // misbehaving server may feed us a different stream after each seek
   247   // operation. So this is really just a hint. The cache may however
   248   // stop reading (suspend the channel) when it thinks we've read all the
   249   // data available based on an incorrect reported length. Seeks relative
   250   // EOF also depend on the reported length if we haven't managed to
   251   // read the whole stream yet.
   252   void NotifyDataLength(int64_t aLength);
   253   // Notifies the cache that a load has begun. We pass the offset
   254   // because in some cases the offset might not be what the cache
   255   // requested. In particular we might unexpectedly start providing
   256   // data at offset 0. This need not be called if the offset is the
   257   // offset that the cache requested in
   258   // ChannelMediaResource::CacheClientSeek. This can be called at any
   259   // time by the client, not just after a CacheClientSeek.
   260   void NotifyDataStarted(int64_t aOffset);
   261   // Notifies the cache that data has been received. The stream already
   262   // knows the offset because data is received in sequence and
   263   // the starting offset is known via NotifyDataStarted or because
   264   // the cache requested the offset in
   265   // ChannelMediaResource::CacheClientSeek, or because it defaulted to 0.
   266   // We pass in the principal that was used to load this data.
   267   void NotifyDataReceived(int64_t aSize, const char* aData,
   268                           nsIPrincipal* aPrincipal);
   269   // Notifies the cache that the current bytes should be written to disk.
   270   // Called on the main thread.
   271   void FlushPartialBlock();
   272   // Notifies the cache that the channel has closed with the given status.
   273   void NotifyDataEnded(nsresult aStatus);
   275   // These methods can be called on any thread.
   276   // Cached blocks associated with this stream will not be evicted
   277   // while the stream is pinned.
   278   void Pin();
   279   void Unpin();
   280   // See comments above for NotifyDataLength about how the length
   281   // can vary over time. Returns -1 if no length is known. Returns the
   282   // reported length if we haven't got any better information. If
   283   // the stream ended normally we return the length we actually got.
   284   // If we've successfully read data beyond the originally reported length,
   285   // we return the end of the data we've read.
   286   int64_t GetLength();
   287   // Returns the unique resource ID. Call only on the main thread or while
   288   // holding the media cache lock.
   289   int64_t GetResourceID() { return mResourceID; }
   290   // Returns the end of the bytes starting at the given offset
   291   // which are in cache.
   292   int64_t GetCachedDataEnd(int64_t aOffset);
   293   // Returns the offset of the first byte of cached data at or after aOffset,
   294   // or -1 if there is no such cached data.
   295   int64_t GetNextCachedData(int64_t aOffset);
   296   // Fills aRanges with the ByteRanges representing the data which is currently
   297   // cached. Locks the media cache while running, to prevent any ranges
   298   // growing. The stream should be pinned while this runs and while its results
   299   // are used, to ensure no data is evicted.
   300   nsresult GetCachedRanges(nsTArray<MediaByteRange>& aRanges);
   302   // Reads from buffered data only. Will fail if not all data to be read is
   303   // in the cache. Will not mark blocks as read. Can be called from the main
   304   // thread. It's the caller's responsibility to wrap the call in a pin/unpin,
   305   // and also to check that the range they want is cached before calling this.
   306   nsresult ReadFromCache(char* aBuffer,
   307                          int64_t aOffset,
   308                          int64_t aCount);
   310   // IsDataCachedToEndOfStream returns true if all the data from
   311   // aOffset to the end of the stream (the server-reported end, if the
   312   // real end is not known) is in cache. If we know nothing about the
   313   // end of the stream, this returns false.
   314   bool IsDataCachedToEndOfStream(int64_t aOffset);
   315   // The mode is initially MODE_PLAYBACK.
   316   void SetReadMode(ReadMode aMode);
   317   // This is the client's estimate of the playback rate assuming
   318   // the media plays continuously. The cache can't guess this itself
   319   // because it doesn't know when the decoder was paused, buffering, etc.
   320   // Do not pass zero.
   321   void SetPlaybackRate(uint32_t aBytesPerSecond);
   322   // Returns the last set value of SetTransportSeekable.
   323   bool IsTransportSeekable();
   325   // Returns true when all streams for this resource are suspended or their
   326   // channel has ended.
   327   bool AreAllStreamsForResourceSuspended();
   329   // These methods must be called on a different thread from the main
   330   // thread. They should always be called on the same thread for a given
   331   // stream.
   332   // This can fail when aWhence is NS_SEEK_END and no stream length
   333   // is known.
   334   nsresult Seek(int32_t aWhence, int64_t aOffset);
   335   int64_t Tell();
   336   // *aBytes gets the number of bytes that were actually read. This can
   337   // be less than aCount. If the first byte of data is not in the cache,
   338   // this will block until the data is available or the stream is
   339   // closed, otherwise it won't block.
   340   nsresult Read(char* aBuffer, uint32_t aCount, uint32_t* aBytes);
   341   // Seeks to aOffset in the stream then performs a Read operation. See
   342   // 'Read' for argument and return details.
   343   nsresult ReadAt(int64_t aOffset, char* aBuffer,
   344                   uint32_t aCount, uint32_t* aBytes);
   346   size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
   348 private:
   349   friend class MediaCache;
   351   /**
   352    * A doubly-linked list of blocks. Add/Remove/Get methods are all
   353    * constant time. We declare this here so that a stream can contain a
   354    * BlockList of its read-ahead blocks. Blocks are referred to by index
   355    * into the MediaCache::mIndex array.
   356    * 
   357    * Blocks can belong to more than one list at the same time, because
   358    * the next/prev pointers are not stored in the block.
   359    */
   360   class BlockList {
   361   public:
   362     BlockList() : mFirstBlock(-1), mCount(0) {}
   363     ~BlockList() {
   364       NS_ASSERTION(mFirstBlock == -1 && mCount == 0,
   365                    "Destroying non-empty block list");
   366     }
   367     void AddFirstBlock(int32_t aBlock);
   368     void AddAfter(int32_t aBlock, int32_t aBefore);
   369     void RemoveBlock(int32_t aBlock);
   370     // Returns the first block in the list, or -1 if empty
   371     int32_t GetFirstBlock() const { return mFirstBlock; }
   372     // Returns the last block in the list, or -1 if empty
   373     int32_t GetLastBlock() const;
   374     // Returns the next block in the list after aBlock or -1 if
   375     // aBlock is the last block
   376     int32_t GetNextBlock(int32_t aBlock) const;
   377     // Returns the previous block in the list before aBlock or -1 if
   378     // aBlock is the first block
   379     int32_t GetPrevBlock(int32_t aBlock) const;
   380     bool IsEmpty() const { return mFirstBlock < 0; }
   381     int32_t GetCount() const { return mCount; }
   382     // The contents of aBlockIndex1 and aBlockIndex2 have been swapped
   383     void NotifyBlockSwapped(int32_t aBlockIndex1, int32_t aBlockIndex2);
   384 #ifdef DEBUG
   385     // Verify linked-list invariants
   386     void Verify();
   387 #else
   388     void Verify() {}
   389 #endif
   391     size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
   393   private:
   394     struct Entry : public nsUint32HashKey {
   395       Entry(KeyTypePointer aKey) : nsUint32HashKey(aKey) { }
   396       Entry(const Entry& toCopy) : nsUint32HashKey(&toCopy.GetKey()),
   397         mNextBlock(toCopy.mNextBlock), mPrevBlock(toCopy.mPrevBlock) {}
   399       int32_t mNextBlock;
   400       int32_t mPrevBlock;
   401     };
   402     nsTHashtable<Entry> mEntries;
   404     // The index of the first block in the list, or -1 if the list is empty.
   405     int32_t mFirstBlock;
   406     // The number of blocks in the list.
   407     int32_t mCount;
   408   };
   410   // Returns the end of the bytes starting at the given offset
   411   // which are in cache.
   412   // This method assumes that the cache monitor is held and can be called on
   413   // any thread.
   414   int64_t GetCachedDataEndInternal(int64_t aOffset);
   415   // Returns the offset of the first byte of cached data at or after aOffset,
   416   // or -1 if there is no such cached data.
   417   // This method assumes that the cache monitor is held and can be called on
   418   // any thread.
   419   int64_t GetNextCachedDataInternal(int64_t aOffset);
   420   // Writes |mPartialBlock| to disk.
   421   // Used by |NotifyDataEnded| and |FlushPartialBlock|.
   422   // If |aNotifyAll| is true, this function will wake up readers who may be
   423   // waiting on the media cache monitor. Called on the main thread only.
   424   void FlushPartialBlockInternal(bool aNotify);
   425   // A helper function to do the work of closing the stream. Assumes
   426   // that the cache monitor is held. Main thread only.
   427   // aReentrantMonitor is the nsAutoReentrantMonitor wrapper holding the cache monitor.
   428   // This is used to NotifyAll to wake up threads that might be
   429   // blocked on reading from this stream.
   430   void CloseInternal(ReentrantMonitorAutoEnter& aReentrantMonitor);
   431   // Update mPrincipal given that data has been received from aPrincipal
   432   bool UpdatePrincipal(nsIPrincipal* aPrincipal);
   434   // These fields are main-thread-only.
   435   ChannelMediaResource*  mClient;
   436   nsCOMPtr<nsIPrincipal> mPrincipal;
   437   // Set to true when Init or InitAsClone has been called
   438   bool                   mInitialized;
   439   // Set to true when MediaCache::Update() has finished while this stream
   440   // was present.
   441   bool                   mHasHadUpdate;
   442   // Set to true when the stream has been closed either explicitly or
   443   // due to an internal cache error
   444   bool                   mClosed;
   445   // True if CacheClientNotifyDataEnded has been called for this stream.
   446   bool                   mDidNotifyDataEnded;
   448   // The following fields must be written holding the cache's monitor and
   449   // only on the main thread, thus can be read either on the main thread
   450   // or while holding the cache's monitor.
   452   // This is a unique ID representing the resource we're loading.
   453   // All streams with the same mResourceID are loading the same
   454   // underlying resource and should share data.
   455   int64_t mResourceID;
   456   // The last reported seekability state for the underlying channel
   457   bool mIsTransportSeekable;
   458   // True if the cache has suspended our channel because the cache is
   459   // full and the priority of the data that would be received is lower
   460   // than the priority of the data already in the cache
   461   bool mCacheSuspended;
   462   // True if the channel ended and we haven't seeked it again.
   463   bool mChannelEnded;
   464   // The offset where the next data from the channel will arrive
   465   int64_t      mChannelOffset;
   466   // The reported or discovered length of the data, or -1 if nothing is
   467   // known
   468   int64_t      mStreamLength;
   470   // The following fields are protected by the cache's monitor can can be written
   471   // by any thread.
   473   // The offset where the reader is positioned in the stream
   474   int64_t           mStreamOffset;
   475   // For each block in the stream data, maps to the cache entry for the
   476   // block, or -1 if the block is not cached.
   477   nsTArray<int32_t> mBlocks;
   478   // The list of read-ahead blocks, ordered by stream offset; the first
   479   // block is the earliest in the stream (so the last block will be the
   480   // least valuable).
   481   BlockList         mReadaheadBlocks;
   482   // The list of metadata blocks; the first block is the most recently used
   483   BlockList         mMetadataBlocks;
   484   // The list of played-back blocks; the first block is the most recently used
   485   BlockList         mPlayedBlocks;
   486   // The last reported estimate of the decoder's playback rate
   487   uint32_t          mPlaybackBytesPerSecond;
   488   // The number of times this stream has been Pinned without a
   489   // corresponding Unpin
   490   uint32_t          mPinCount;
   491   // The status used when we did CacheClientNotifyDataEnded. Only valid
   492   // when mDidNotifyDataEnded is true.
   493   nsresult          mNotifyDataEndedStatus;
   494   // The last reported read mode
   495   ReadMode          mCurrentMode;
   496   // True if some data in mPartialBlockBuffer has been read as metadata
   497   bool              mMetadataInPartialBlockBuffer;
   499   // The following field is protected by the cache's monitor but are
   500   // only written on the main thread.
   502   // Data received for the block containing mChannelOffset. Data needs
   503   // to wait here so we can write back a complete block. The first
   504   // mChannelOffset%BLOCK_SIZE bytes have been filled in with good data,
   505   // the rest are garbage.
   506   // Use int64_t so that the data is well-aligned.
   507   // Heap allocate this buffer since the exact power-of-2 will cause allocation
   508   // slop when combined with the rest of the object members.
   509   nsAutoArrayPtr<int64_t> mPartialBlockBuffer;
   510 };
   512 } // namespace mozilla
   514 #endif

mercurial