summaryrefslogtreecommitdiffstats
path: root/image/SurfaceCache.cpp
blob: 66fdfcca049c1849c9ea13a651bd5ecf9df8e557 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * SurfaceCache is a service for caching temporary surfaces in imagelib.
 */

#include "SurfaceCache.h"

#include <algorithm>
#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/Likely.h"
#include "mozilla/Move.h"
#include "mozilla/Pair.h"
#include "mozilla/RefPtr.h"
#include "mozilla/StaticMutex.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/Tuple.h"
#include "nsIMemoryReporter.h"
#include "gfx2DGlue.h"
#include "gfxPlatform.h"
#include "gfxPrefs.h"
#include "imgFrame.h"
#include "Image.h"
#include "ISurfaceProvider.h"
#include "LookupResult.h"
#include "nsExpirationTracker.h"
#include "nsHashKeys.h"
#include "nsRefPtrHashtable.h"
#include "nsSize.h"
#include "nsTArray.h"
#include "prsystem.h"
#include "ShutdownTracker.h"

using std::max;
using std::min;

namespace mozilla {

using namespace gfx;

namespace image {

class CachedSurface;
class SurfaceCacheImpl;

///////////////////////////////////////////////////////////////////////////////
// Static Data
///////////////////////////////////////////////////////////////////////////////

// The single surface cache instance.
static StaticRefPtr<SurfaceCacheImpl> sInstance;

// The mutex protecting the surface cache.
static StaticMutex sInstanceMutex;

///////////////////////////////////////////////////////////////////////////////
// SurfaceCache Implementation
///////////////////////////////////////////////////////////////////////////////

/**
 * Cost models the cost of storing a surface in the cache. Right now, this is
 * simply an estimate of the size of the surface in bytes, but in the future it
 * may be worth taking into account the cost of rematerializing the surface as
 * well.
 */
typedef size_t Cost;

static Cost
ComputeCost(const IntSize& aSize, uint32_t aBytesPerPixel)
{
  MOZ_ASSERT(aBytesPerPixel == 1 || aBytesPerPixel == 4);
  return aSize.width * aSize.height * aBytesPerPixel;
}

/**
 * Since we want to be able to make eviction decisions based on cost, we need to
 * be able to look up the CachedSurface which has a certain cost as well as the
 * cost associated with a certain CachedSurface. To make this possible, in data
 * structures we actually store a CostEntry, which contains a weak pointer to
 * its associated surface.
 *
 * To make usage of the weak pointer safe, SurfaceCacheImpl always calls
 * StartTracking after a surface is stored in the cache and StopTracking before
 * it is removed.
 */
class CostEntry
{
public:
  CostEntry(NotNull<CachedSurface*> aSurface, Cost aCost)
    : mSurface(aSurface)
    , mCost(aCost)
  { }

  NotNull<CachedSurface*> Surface() const { return mSurface; }
  Cost GetCost() const { return mCost; }

  bool operator==(const CostEntry& aOther) const
  {
    return mSurface == aOther.mSurface &&
           mCost == aOther.mCost;
  }

  bool operator<(const CostEntry& aOther) const
  {
    return mCost < aOther.mCost ||
           (mCost == aOther.mCost && mSurface < aOther.mSurface);
  }

private:
  NotNull<CachedSurface*> mSurface;
  Cost                    mCost;
};

/**
 * A CachedSurface associates a surface with a key that uniquely identifies that
 * surface.
 */
class CachedSurface
{
  ~CachedSurface() { }
public:
  MOZ_DECLARE_REFCOUNTED_TYPENAME(CachedSurface)
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CachedSurface)

  explicit CachedSurface(NotNull<ISurfaceProvider*> aProvider)
    : mProvider(aProvider)
    , mIsLocked(false)
  { }

  DrawableSurface GetDrawableSurface() const
  {
    if (MOZ_UNLIKELY(IsPlaceholder())) {
      MOZ_ASSERT_UNREACHABLE("Called GetDrawableSurface() on a placeholder");
      return DrawableSurface();
    }

    return mProvider->Surface();
  }

  void SetLocked(bool aLocked)
  {
    if (IsPlaceholder()) {
      return;  // Can't lock a placeholder.
    }

    // Update both our state and our provider's state. Some surface providers
    // are permanently locked; maintaining our own locking state enables us to
    // respect SetLocked() even when it's meaningless from the provider's
    // perspective.
    mIsLocked = aLocked;
    mProvider->SetLocked(aLocked);
  }

  bool IsLocked() const
  {
    return !IsPlaceholder() && mIsLocked && mProvider->IsLocked();
  }

  bool IsPlaceholder() const { return mProvider->Availability().IsPlaceholder(); }
  bool IsDecoded() const { return !IsPlaceholder() && mProvider->IsFinished(); }

  ImageKey GetImageKey() const { return mProvider->GetImageKey(); }
  SurfaceKey GetSurfaceKey() const { return mProvider->GetSurfaceKey(); }
  nsExpirationState* GetExpirationState() { return &mExpirationState; }

  CostEntry GetCostEntry()
  {
    return image::CostEntry(WrapNotNull(this), mProvider->LogicalSizeInBytes());
  }

  // A helper type used by SurfaceCacheImpl::CollectSizeOfSurfaces.
  struct MOZ_STACK_CLASS SurfaceMemoryReport
  {
    SurfaceMemoryReport(nsTArray<SurfaceMemoryCounter>& aCounters,
                        MallocSizeOf                    aMallocSizeOf)
      : mCounters(aCounters)
      , mMallocSizeOf(aMallocSizeOf)
    { }

    void Add(NotNull<CachedSurface*> aCachedSurface)
    {
      SurfaceMemoryCounter counter(aCachedSurface->GetSurfaceKey(),
                                   aCachedSurface->IsLocked());

      if (aCachedSurface->IsPlaceholder()) {
        return;
      }

      // Record the memory used by the ISurfaceProvider. This may not have a
      // straightforward relationship to the size of the surface that
      // DrawableRef() returns if the surface is generated dynamically. (i.e.,
      // for surfaces with PlaybackType::eAnimated.)
      size_t heap = 0;
      size_t nonHeap = 0;
      aCachedSurface->mProvider
        ->AddSizeOfExcludingThis(mMallocSizeOf, heap, nonHeap);
      counter.Values().SetDecodedHeap(heap);
      counter.Values().SetDecodedNonHeap(nonHeap);

      mCounters.AppendElement(counter);
    }

  private:
    nsTArray<SurfaceMemoryCounter>& mCounters;
    MallocSizeOf                    mMallocSizeOf;
  };

private:
  nsExpirationState                 mExpirationState;
  NotNull<RefPtr<ISurfaceProvider>> mProvider;
  bool                              mIsLocked;
};

static int64_t
AreaOfIntSize(const IntSize& aSize) {
  return static_cast<int64_t>(aSize.width) * static_cast<int64_t>(aSize.height);
}

/**
 * An ImageSurfaceCache is a per-image surface cache. For correctness we must be
 * able to remove all surfaces associated with an image when the image is
 * destroyed or invalidated. Since this will happen frequently, it makes sense
 * to make it cheap by storing the surfaces for each image separately.
 *
 * ImageSurfaceCache also keeps track of whether its associated image is locked
 * or unlocked.
 */
class ImageSurfaceCache
{
  ~ImageSurfaceCache() { }
public:
  ImageSurfaceCache() : mLocked(false) { }

  MOZ_DECLARE_REFCOUNTED_TYPENAME(ImageSurfaceCache)
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ImageSurfaceCache)

  typedef
    nsRefPtrHashtable<nsGenericHashKey<SurfaceKey>, CachedSurface> SurfaceTable;

  bool IsEmpty() const { return mSurfaces.Count() == 0; }

  void Insert(NotNull<CachedSurface*> aSurface)
  {
    MOZ_ASSERT(!mLocked || aSurface->IsPlaceholder() || aSurface->IsLocked(),
               "Inserting an unlocked surface for a locked image");
    mSurfaces.Put(aSurface->GetSurfaceKey(), aSurface);
  }

  void Remove(NotNull<CachedSurface*> aSurface)
  {
    MOZ_ASSERT(mSurfaces.GetWeak(aSurface->GetSurfaceKey()),
        "Should not be removing a surface we don't have");

    mSurfaces.Remove(aSurface->GetSurfaceKey());
  }

  already_AddRefed<CachedSurface> Lookup(const SurfaceKey& aSurfaceKey)
  {
    RefPtr<CachedSurface> surface;
    mSurfaces.Get(aSurfaceKey, getter_AddRefs(surface));
    return surface.forget();
  }

  Pair<already_AddRefed<CachedSurface>, MatchType>
  LookupBestMatch(const SurfaceKey& aIdealKey)
  {
    // Try for an exact match first.
    RefPtr<CachedSurface> exactMatch;
    mSurfaces.Get(aIdealKey, getter_AddRefs(exactMatch));
    if (exactMatch && exactMatch->IsDecoded()) {
      return MakePair(exactMatch.forget(), MatchType::EXACT);
    }

    // There's no perfect match, so find the best match we can.
    RefPtr<CachedSurface> bestMatch;
    for (auto iter = ConstIter(); !iter.Done(); iter.Next()) {
      NotNull<CachedSurface*> current = WrapNotNull(iter.UserData());
      const SurfaceKey& currentKey = current->GetSurfaceKey();

      // We never match a placeholder.
      if (current->IsPlaceholder()) {
        continue;
      }
      // Matching the playback type and SVG context is required.
      if (currentKey.Playback() != aIdealKey.Playback() ||
          currentKey.SVGContext() != aIdealKey.SVGContext()) {
        continue;
      }
      // Matching the flags is required.
      if (currentKey.Flags() != aIdealKey.Flags()) {
        continue;
      }
      // Anything is better than nothing! (Within the constraints we just
      // checked, of course.)
      if (!bestMatch) {
        bestMatch = current;
        continue;
      }

      MOZ_ASSERT(bestMatch, "Should have a current best match");

      // Always prefer completely decoded surfaces.
      bool bestMatchIsDecoded = bestMatch->IsDecoded();
      if (bestMatchIsDecoded && !current->IsDecoded()) {
        continue;
      }
      if (!bestMatchIsDecoded && current->IsDecoded()) {
        bestMatch = current;
        continue;
      }

      SurfaceKey bestMatchKey = bestMatch->GetSurfaceKey();

      // Compare sizes. We use an area-based heuristic here instead of computing a
      // truly optimal answer, since it seems very unlikely to make a difference
      // for realistic sizes.
      int64_t idealArea = AreaOfIntSize(aIdealKey.Size());
      int64_t currentArea = AreaOfIntSize(currentKey.Size());
      int64_t bestMatchArea = AreaOfIntSize(bestMatchKey.Size());

      // If the best match is smaller than the ideal size, prefer bigger sizes.
      if (bestMatchArea < idealArea) {
        if (currentArea > bestMatchArea) {
          bestMatch = current;
        }
        continue;
      }
      // Other, prefer sizes closer to the ideal size, but still not smaller.
      if (idealArea <= currentArea && currentArea < bestMatchArea) {
        bestMatch = current;
        continue;
      }
      // This surface isn't an improvement over the current best match.
    }

    MatchType matchType;
    if (bestMatch) {
      if (!exactMatch) {
        // No exact match, but we found a substitute.
        matchType = MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND;
      } else if (exactMatch != bestMatch) {
        // The exact match is still decoding, but we found a substitute.
        matchType = MatchType::SUBSTITUTE_BECAUSE_PENDING;
      } else {
        // The exact match is still decoding, but it's the best we've got.
        matchType = MatchType::EXACT;
      }
    } else {
      if (exactMatch) {
        // We found an "exact match"; it must have been a placeholder.
        MOZ_ASSERT(exactMatch->IsPlaceholder());
        matchType = MatchType::PENDING;
      } else {
        // We couldn't find an exact match *or* a substitute.
        matchType = MatchType::NOT_FOUND;
      }
    }

    return MakePair(bestMatch.forget(), matchType);
  }

  SurfaceTable::Iterator ConstIter() const
  {
    return mSurfaces.ConstIter();
  }

  void SetLocked(bool aLocked) { mLocked = aLocked; }
  bool IsLocked() const { return mLocked; }

private:
  SurfaceTable mSurfaces;
  bool         mLocked;
};

/**
 * SurfaceCacheImpl is responsible for determining which surfaces will be cached
 * and managing the surface cache data structures. Rather than interact with
 * SurfaceCacheImpl directly, client code interacts with SurfaceCache, which
 * maintains high-level invariants and encapsulates the details of the surface
 * cache's implementation.
 */
class SurfaceCacheImpl final : public nsIMemoryReporter
{
public:
  NS_DECL_ISUPPORTS

  SurfaceCacheImpl(uint32_t aSurfaceCacheExpirationTimeMS,
                   uint32_t aSurfaceCacheDiscardFactor,
                   uint32_t aSurfaceCacheSize)
    : mExpirationTracker(aSurfaceCacheExpirationTimeMS)
    , mMemoryPressureObserver(new MemoryPressureObserver)
    , mDiscardFactor(aSurfaceCacheDiscardFactor)
    , mMaxCost(aSurfaceCacheSize)
    , mAvailableCost(aSurfaceCacheSize)
    , mLockedCost(0)
    , mOverflowCount(0)
  {
    nsCOMPtr<nsIObserverService> os = services::GetObserverService();
    if (os) {
      os->AddObserver(mMemoryPressureObserver, "memory-pressure", false);
    }
  }

private:
  virtual ~SurfaceCacheImpl()
  {
    nsCOMPtr<nsIObserverService> os = services::GetObserverService();
    if (os) {
      os->RemoveObserver(mMemoryPressureObserver, "memory-pressure");
    }

    UnregisterWeakMemoryReporter(this);
  }

public:
  void InitMemoryReporter() { RegisterWeakMemoryReporter(this); }

  InsertOutcome Insert(NotNull<ISurfaceProvider*> aProvider,
                       bool                       aSetAvailable,
                       const StaticMutexAutoLock& aAutoLock)
  {
    // If this is a duplicate surface, refuse to replace the original.
    // XXX(seth): Calling Lookup() and then RemoveEntry() does the lookup
    // twice. We'll make this more efficient in bug 1185137.
    LookupResult result = Lookup(aProvider->GetImageKey(),
                                 aProvider->GetSurfaceKey(),
                                 aAutoLock,
                                 /* aMarkUsed = */ false);
    if (MOZ_UNLIKELY(result)) {
      return InsertOutcome::FAILURE_ALREADY_PRESENT;
    }

    if (result.Type() == MatchType::PENDING) {
      RemoveEntry(aProvider->GetImageKey(), aProvider->GetSurfaceKey(), aAutoLock);
    }

    MOZ_ASSERT(result.Type() == MatchType::NOT_FOUND ||
               result.Type() == MatchType::PENDING,
               "A LookupResult with no surface should be NOT_FOUND or PENDING");

    // If this is bigger than we can hold after discarding everything we can,
    // refuse to cache it.
    Cost cost = aProvider->LogicalSizeInBytes();
    if (MOZ_UNLIKELY(!CanHoldAfterDiscarding(cost))) {
      mOverflowCount++;
      return InsertOutcome::FAILURE;
    }

    // Remove elements in order of cost until we can fit this in the cache. Note
    // that locked surfaces aren't in mCosts, so we never remove them here.
    while (cost > mAvailableCost) {
      MOZ_ASSERT(!mCosts.IsEmpty(),
                 "Removed everything and it still won't fit");
      Remove(mCosts.LastElement().Surface(), aAutoLock);
    }

    // Locate the appropriate per-image cache. If there's not an existing cache
    // for this image, create it.
    RefPtr<ImageSurfaceCache> cache = GetImageCache(aProvider->GetImageKey());
    if (!cache) {
      cache = new ImageSurfaceCache;
      mImageCaches.Put(aProvider->GetImageKey(), cache);
    }

    // If we were asked to mark the cache entry available, do so.
    if (aSetAvailable) {
      aProvider->Availability().SetAvailable();
    }

    NotNull<RefPtr<CachedSurface>> surface =
      WrapNotNull(new CachedSurface(aProvider));

    // We require that locking succeed if the image is locked and we're not
    // inserting a placeholder; the caller may need to know this to handle
    // errors correctly.
    if (cache->IsLocked() && !surface->IsPlaceholder()) {
      surface->SetLocked(true);
      if (!surface->IsLocked()) {
        return InsertOutcome::FAILURE;
      }
    }

    // Insert.
    MOZ_ASSERT(cost <= mAvailableCost, "Inserting despite too large a cost");
    cache->Insert(surface);
    StartTracking(surface, aAutoLock);

    return InsertOutcome::SUCCESS;
  }

  void Remove(NotNull<CachedSurface*> aSurface,
              const StaticMutexAutoLock& aAutoLock)
  {
    ImageKey imageKey = aSurface->GetImageKey();

    RefPtr<ImageSurfaceCache> cache = GetImageCache(imageKey);
    MOZ_ASSERT(cache, "Shouldn't try to remove a surface with no image cache");

    // If the surface was not a placeholder, tell its image that we discarded it.
    if (!aSurface->IsPlaceholder()) {
      static_cast<Image*>(imageKey)->OnSurfaceDiscarded();
    }

    StopTracking(aSurface, aAutoLock);
    cache->Remove(aSurface);

    // Remove the per-image cache if it's unneeded now. (Keep it if the image is
    // locked, since the per-image cache is where we store that state.)
    if (cache->IsEmpty() && !cache->IsLocked()) {
      mImageCaches.Remove(imageKey);
    }
  }

  void StartTracking(NotNull<CachedSurface*> aSurface,
                     const StaticMutexAutoLock& aAutoLock)
  {
    CostEntry costEntry = aSurface->GetCostEntry();
    MOZ_ASSERT(costEntry.GetCost() <= mAvailableCost,
               "Cost too large and the caller didn't catch it");

    mAvailableCost -= costEntry.GetCost();

    if (aSurface->IsLocked()) {
      mLockedCost += costEntry.GetCost();
      MOZ_ASSERT(mLockedCost <= mMaxCost, "Locked more than we can hold?");
    } else {
      mCosts.InsertElementSorted(costEntry);
      // This may fail during XPCOM shutdown, so we need to ensure the object is
      // tracked before calling RemoveObject in StopTracking.
      mExpirationTracker.AddObjectLocked(aSurface, aAutoLock);
    }
  }

  void StopTracking(NotNull<CachedSurface*> aSurface,
                    const StaticMutexAutoLock& aAutoLock)
  {
    CostEntry costEntry = aSurface->GetCostEntry();

    if (aSurface->IsLocked()) {
      MOZ_ASSERT(mLockedCost >= costEntry.GetCost(), "Costs don't balance");
      mLockedCost -= costEntry.GetCost();
      // XXX(seth): It'd be nice to use an O(log n) lookup here. This is O(n).
      MOZ_ASSERT(!mCosts.Contains(costEntry),
                 "Shouldn't have a cost entry for a locked surface");
    } else {
      if (MOZ_LIKELY(aSurface->GetExpirationState()->IsTracked())) {
        mExpirationTracker.RemoveObjectLocked(aSurface, aAutoLock);
      } else {
        // Our call to AddObject must have failed in StartTracking; most likely
        // we're in XPCOM shutdown right now.
        NS_ASSERTION(ShutdownTracker::ShutdownHasStarted(),
                     "Not expiration-tracking an unlocked surface!");
      }

      DebugOnly<bool> foundInCosts = mCosts.RemoveElementSorted(costEntry);
      MOZ_ASSERT(foundInCosts, "Lost track of costs for this surface");
    }

    mAvailableCost += costEntry.GetCost();
    MOZ_ASSERT(mAvailableCost <= mMaxCost,
               "More available cost than we started with");
  }

  LookupResult Lookup(const ImageKey    aImageKey,
                      const SurfaceKey& aSurfaceKey,
                      const StaticMutexAutoLock& aAutoLock,
                      bool aMarkUsed = true)
  {
    RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
    if (!cache) {
      // No cached surfaces for this image.
      return LookupResult(MatchType::NOT_FOUND);
    }

    RefPtr<CachedSurface> surface = cache->Lookup(aSurfaceKey);
    if (!surface) {
      // Lookup in the per-image cache missed.
      return LookupResult(MatchType::NOT_FOUND);
    }

    if (surface->IsPlaceholder()) {
      return LookupResult(MatchType::PENDING);
    }

    DrawableSurface drawableSurface = surface->GetDrawableSurface();
    if (!drawableSurface) {
      // The surface was released by the operating system. Remove the cache
      // entry as well.
      Remove(WrapNotNull(surface), aAutoLock);
      return LookupResult(MatchType::NOT_FOUND);
    }

    if (aMarkUsed) {
      MarkUsed(WrapNotNull(surface), WrapNotNull(cache), aAutoLock);
    }

    MOZ_ASSERT(surface->GetSurfaceKey() == aSurfaceKey,
               "Lookup() not returning an exact match?");
    return LookupResult(Move(drawableSurface), MatchType::EXACT);
  }

  LookupResult LookupBestMatch(const ImageKey         aImageKey,
                               const SurfaceKey&      aSurfaceKey,
                               const StaticMutexAutoLock& aAutoLock)
  {
    RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
    if (!cache) {
      // No cached surfaces for this image.
      return LookupResult(MatchType::NOT_FOUND);
    }

    // Repeatedly look up the best match, trying again if the resulting surface
    // has been freed by the operating system, until we can either lock a
    // surface for drawing or there are no matching surfaces left.
    // XXX(seth): This is O(N^2), but N is expected to be very small. If we
    // encounter a performance problem here we can revisit this.

    RefPtr<CachedSurface> surface;
    DrawableSurface drawableSurface;
    MatchType matchType = MatchType::NOT_FOUND;
    while (true) {
      Tie(surface, matchType) = cache->LookupBestMatch(aSurfaceKey);

      if (!surface) {
        return LookupResult(matchType);  // Lookup in the per-image cache missed.
      }

      drawableSurface = surface->GetDrawableSurface();
      if (drawableSurface) {
        break;
      }

      // The surface was released by the operating system. Remove the cache
      // entry as well.
      Remove(WrapNotNull(surface), aAutoLock);
    }

    MOZ_ASSERT_IF(matchType == MatchType::EXACT,
                  surface->GetSurfaceKey() == aSurfaceKey);
    MOZ_ASSERT_IF(matchType == MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND ||
                  matchType == MatchType::SUBSTITUTE_BECAUSE_PENDING,
      surface->GetSurfaceKey().SVGContext() == aSurfaceKey.SVGContext() &&
      surface->GetSurfaceKey().Playback() == aSurfaceKey.Playback() &&
      surface->GetSurfaceKey().Flags() == aSurfaceKey.Flags());

    if (matchType == MatchType::EXACT) {
      MarkUsed(WrapNotNull(surface), WrapNotNull(cache), aAutoLock);
    }

    return LookupResult(Move(drawableSurface), matchType);
  }

  bool CanHold(const Cost aCost) const
  {
    return aCost <= mMaxCost;
  }

  size_t MaximumCapacity() const
  {
    return size_t(mMaxCost);
  }

  void SurfaceAvailable(NotNull<ISurfaceProvider*> aProvider,
                        const StaticMutexAutoLock& aAutoLock)
  {
    if (!aProvider->Availability().IsPlaceholder()) {
      MOZ_ASSERT_UNREACHABLE("Calling SurfaceAvailable on non-placeholder");
      return;
    }

    // Reinsert the provider, requesting that Insert() mark it available. This
    // may or may not succeed, depending on whether some other decoder has
    // beaten us to the punch and inserted a non-placeholder version of this
    // surface first, but it's fine either way.
    // XXX(seth): This could be implemented more efficiently; we should be able
    // to just update our data structures without reinserting.
    Insert(aProvider, /* aSetAvailable = */ true, aAutoLock);
  }

  void LockImage(const ImageKey aImageKey)
  {
    RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
    if (!cache) {
      cache = new ImageSurfaceCache;
      mImageCaches.Put(aImageKey, cache);
    }

    cache->SetLocked(true);

    // We don't relock this image's existing surfaces right away; instead, the
    // image should arrange for Lookup() to touch them if they are still useful.
  }

  void UnlockImage(const ImageKey aImageKey, const StaticMutexAutoLock& aAutoLock)
  {
    RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
    if (!cache || !cache->IsLocked()) {
      return;  // Already unlocked.
    }

    cache->SetLocked(false);
    DoUnlockSurfaces(WrapNotNull(cache), aAutoLock);
  }

  void UnlockEntries(const ImageKey aImageKey, const StaticMutexAutoLock& aAutoLock)
  {
    RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
    if (!cache || !cache->IsLocked()) {
      return;  // Already unlocked.
    }

    // (Note that we *don't* unlock the per-image cache here; that's the
    // difference between this and UnlockImage.)
    DoUnlockSurfaces(WrapNotNull(cache), aAutoLock);
  }

  void RemoveImage(const ImageKey aImageKey, const StaticMutexAutoLock& aAutoLock)
  {
    RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
    if (!cache) {
      return;  // No cached surfaces for this image, so nothing to do.
    }

    // Discard all of the cached surfaces for this image.
    // XXX(seth): This is O(n^2) since for each item in the cache we are
    // removing an element from the costs array. Since n is expected to be
    // small, performance should be good, but if usage patterns change we should
    // change the data structure used for mCosts.
    for (auto iter = cache->ConstIter(); !iter.Done(); iter.Next()) {
      StopTracking(WrapNotNull(iter.UserData()), aAutoLock);
    }

    // The per-image cache isn't needed anymore, so remove it as well.
    // This implicitly unlocks the image if it was locked.
    mImageCaches.Remove(aImageKey);
  }

  void DiscardAll(const StaticMutexAutoLock& aAutoLock)
  {
    // Remove in order of cost because mCosts is an array and the other data
    // structures are all hash tables. Note that locked surfaces are not
    // removed, since they aren't present in mCosts.
    while (!mCosts.IsEmpty()) {
      Remove(mCosts.LastElement().Surface(), aAutoLock);
    }
  }

  void DiscardForMemoryPressure(const StaticMutexAutoLock& aAutoLock)
  {
    // Compute our discardable cost. Since locked surfaces aren't discardable,
    // we exclude them.
    const Cost discardableCost = (mMaxCost - mAvailableCost) - mLockedCost;
    MOZ_ASSERT(discardableCost <= mMaxCost, "Discardable cost doesn't add up");

    // Our target is to raise our available cost by (1 / mDiscardFactor) of our
    // discardable cost - in other words, we want to end up with about
    // (discardableCost / mDiscardFactor) fewer bytes stored in the surface
    // cache after we're done.
    const Cost targetCost = mAvailableCost + (discardableCost / mDiscardFactor);

    if (targetCost > mMaxCost - mLockedCost) {
      MOZ_ASSERT_UNREACHABLE("Target cost is more than we can discard");
      DiscardAll(aAutoLock);
      return;
    }

    // Discard surfaces until we've reduced our cost to our target cost.
    while (mAvailableCost < targetCost) {
      MOZ_ASSERT(!mCosts.IsEmpty(), "Removed everything and still not done");
      Remove(mCosts.LastElement().Surface(), aAutoLock);
    }
  }

  void LockSurface(NotNull<CachedSurface*> aSurface,
                   const StaticMutexAutoLock& aAutoLock)
  {
    if (aSurface->IsPlaceholder() || aSurface->IsLocked()) {
      return;
    }

    StopTracking(aSurface, aAutoLock);

    // Lock the surface. This can fail.
    aSurface->SetLocked(true);
    StartTracking(aSurface, aAutoLock);
  }

  NS_IMETHOD
  CollectReports(nsIHandleReportCallback* aHandleReport,
                 nsISupports*             aData,
                 bool                     aAnonymize) override
  {
    StaticMutexAutoLock lock(sInstanceMutex);

    // We have explicit memory reporting for the surface cache which is more
    // accurate than the cost metrics we report here, but these metrics are
    // still useful to report, since they control the cache's behavior.
    MOZ_COLLECT_REPORT(
      "imagelib-surface-cache-estimated-total",
      KIND_OTHER, UNITS_BYTES, (mMaxCost - mAvailableCost),
"Estimated total memory used by the imagelib surface cache.");

    MOZ_COLLECT_REPORT(
      "imagelib-surface-cache-estimated-locked",
      KIND_OTHER, UNITS_BYTES, mLockedCost,
"Estimated memory used by locked surfaces in the imagelib surface cache.");

    MOZ_COLLECT_REPORT(
      "imagelib-surface-cache-overflow-count",
      KIND_OTHER, UNITS_COUNT, mOverflowCount,
"Count of how many times the surface cache has hit its capacity and been "
"unable to insert a new surface.");

    return NS_OK;
  }

  void CollectSizeOfSurfaces(const ImageKey                  aImageKey,
                             nsTArray<SurfaceMemoryCounter>& aCounters,
                             MallocSizeOf                    aMallocSizeOf)
  {
    RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
    if (!cache) {
      return;  // No surfaces for this image.
    }

    // Report all surfaces in the per-image cache.
    CachedSurface::SurfaceMemoryReport report(aCounters, aMallocSizeOf);
    for (auto iter = cache->ConstIter(); !iter.Done(); iter.Next()) {
      report.Add(WrapNotNull(iter.UserData()));
    }
  }

private:
  already_AddRefed<ImageSurfaceCache> GetImageCache(const ImageKey aImageKey)
  {
    RefPtr<ImageSurfaceCache> imageCache;
    mImageCaches.Get(aImageKey, getter_AddRefs(imageCache));
    return imageCache.forget();
  }

  // This is similar to CanHold() except that it takes into account the costs of
  // locked surfaces. It's used internally in Insert(), but it's not exposed
  // publicly because we permit multithreaded access to the surface cache, which
  // means that the result would be meaningless: another thread could insert a
  // surface or lock an image at any time.
  bool CanHoldAfterDiscarding(const Cost aCost) const
  {
    return aCost <= mMaxCost - mLockedCost;
  }

  void MarkUsed(NotNull<CachedSurface*> aSurface,
                NotNull<ImageSurfaceCache*> aCache,
                const StaticMutexAutoLock& aAutoLock)
  {
    if (aCache->IsLocked()) {
      LockSurface(aSurface, aAutoLock);
    } else {
      mExpirationTracker.MarkUsedLocked(aSurface, aAutoLock);
    }
  }

  void DoUnlockSurfaces(NotNull<ImageSurfaceCache*> aCache,
                        const StaticMutexAutoLock& aAutoLock)
  {
    // Unlock all the surfaces the per-image cache is holding.
    for (auto iter = aCache->ConstIter(); !iter.Done(); iter.Next()) {
      NotNull<CachedSurface*> surface = WrapNotNull(iter.UserData());
      if (surface->IsPlaceholder() || !surface->IsLocked()) {
        continue;
      }
      StopTracking(surface, aAutoLock);
      surface->SetLocked(false);
      StartTracking(surface, aAutoLock);
    }
  }

  void RemoveEntry(const ImageKey    aImageKey,
                   const SurfaceKey& aSurfaceKey,
                   const StaticMutexAutoLock& aAutoLock)
  {
    RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
    if (!cache) {
      return;  // No cached surfaces for this image.
    }

    RefPtr<CachedSurface> surface = cache->Lookup(aSurfaceKey);
    if (!surface) {
      return;  // Lookup in the per-image cache missed.
    }

    Remove(WrapNotNull(surface), aAutoLock);
  }

  struct SurfaceTracker : public ExpirationTrackerImpl<CachedSurface, 2,
                                                       StaticMutex,
                                                       StaticMutexAutoLock>
  {
    explicit SurfaceTracker(uint32_t aSurfaceCacheExpirationTimeMS)
      : ExpirationTrackerImpl<CachedSurface, 2,
                              StaticMutex, StaticMutexAutoLock>(
          aSurfaceCacheExpirationTimeMS, "SurfaceTracker")
    { }

  protected:
    void NotifyExpiredLocked(CachedSurface* aSurface,
                             const StaticMutexAutoLock& aAutoLock) override
    {
      sInstance->Remove(WrapNotNull(aSurface), aAutoLock);
    }

    StaticMutex& GetMutex() override
    {
      return sInstanceMutex;
    }
  };

  struct MemoryPressureObserver : public nsIObserver
  {
    NS_DECL_ISUPPORTS

    NS_IMETHOD Observe(nsISupports*,
                       const char* aTopic,
                       const char16_t*) override
    {
      StaticMutexAutoLock lock(sInstanceMutex);
      if (sInstance && strcmp(aTopic, "memory-pressure") == 0) {
        sInstance->DiscardForMemoryPressure(lock);
      }
      return NS_OK;
    }

  private:
    virtual ~MemoryPressureObserver() { }
  };

  nsTArray<CostEntry>                     mCosts;
  nsRefPtrHashtable<nsPtrHashKey<Image>,
    ImageSurfaceCache> mImageCaches;
  SurfaceTracker                          mExpirationTracker;
  RefPtr<MemoryPressureObserver>        mMemoryPressureObserver;
  const uint32_t                          mDiscardFactor;
  const Cost                              mMaxCost;
  Cost                                    mAvailableCost;
  Cost                                    mLockedCost;
  size_t                                  mOverflowCount;
};

NS_IMPL_ISUPPORTS(SurfaceCacheImpl, nsIMemoryReporter)
NS_IMPL_ISUPPORTS(SurfaceCacheImpl::MemoryPressureObserver, nsIObserver)

///////////////////////////////////////////////////////////////////////////////
// Public API
///////////////////////////////////////////////////////////////////////////////

/* static */ void
SurfaceCache::Initialize()
{
  // Initialize preferences.
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(!sInstance, "Shouldn't initialize more than once");

  // See gfxPrefs for the default values of these preferences.

  // Length of time before an unused surface is removed from the cache, in
  // milliseconds.
  uint32_t surfaceCacheExpirationTimeMS =
    gfxPrefs::ImageMemSurfaceCacheMinExpirationMS();

  // What fraction of the memory used by the surface cache we should discard
  // when we get a memory pressure notification. This value is interpreted as
  // 1/N, so 1 means to discard everything, 2 means to discard about half of the
  // memory we're using, and so forth. We clamp it to avoid division by zero.
  uint32_t surfaceCacheDiscardFactor =
    max(gfxPrefs::ImageMemSurfaceCacheDiscardFactor(), 1u);

  // Maximum size of the surface cache, in kilobytes.
  uint64_t surfaceCacheMaxSizeKB = gfxPrefs::ImageMemSurfaceCacheMaxSizeKB();

  // A knob determining the actual size of the surface cache. Currently the
  // cache is (size of main memory) / (surface cache size factor) KB
  // or (surface cache max size) KB, whichever is smaller. The formula
  // may change in the future, though.
  // For example, a value of 4 would yield a 256MB cache on a 1GB machine.
  // The smallest machines we are likely to run this code on have 256MB
  // of memory, which would yield a 64MB cache on this setting.
  // We clamp this value to avoid division by zero.
  uint32_t surfaceCacheSizeFactor =
    max(gfxPrefs::ImageMemSurfaceCacheSizeFactor(), 1u);

  // Compute the size of the surface cache.
  uint64_t memorySize = PR_GetPhysicalMemorySize();
  if (memorySize == 0) {
    MOZ_ASSERT_UNREACHABLE("PR_GetPhysicalMemorySize not implemented here");
    memorySize = 256 * 1024 * 1024;  // Fall back to 256MB.
  }
  uint64_t proposedSize = memorySize / surfaceCacheSizeFactor;
  uint64_t surfaceCacheSizeBytes = min(proposedSize,
                                       surfaceCacheMaxSizeKB * 1024);
  uint32_t finalSurfaceCacheSizeBytes =
    min(surfaceCacheSizeBytes, uint64_t(UINT32_MAX));

  // Create the surface cache singleton with the requested settings.  Note that
  // the size is a limit that the cache may not grow beyond, but we do not
  // actually allocate any storage for surfaces at this time.
  sInstance = new SurfaceCacheImpl(surfaceCacheExpirationTimeMS,
                                   surfaceCacheDiscardFactor,
                                   finalSurfaceCacheSizeBytes);
  sInstance->InitMemoryReporter();
}

/* static */ void
SurfaceCache::Shutdown()
{
  StaticMutexAutoLock lock(sInstanceMutex);
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(sInstance, "No singleton - was Shutdown() called twice?");
  sInstance = nullptr;
}

/* static */ LookupResult
SurfaceCache::Lookup(const ImageKey         aImageKey,
                     const SurfaceKey&      aSurfaceKey)
{
  StaticMutexAutoLock lock(sInstanceMutex);
  if (!sInstance) {
    return LookupResult(MatchType::NOT_FOUND);
  }

  return sInstance->Lookup(aImageKey, aSurfaceKey, lock);
}

/* static */ LookupResult
SurfaceCache::LookupBestMatch(const ImageKey         aImageKey,
                              const SurfaceKey&      aSurfaceKey)
{
  StaticMutexAutoLock lock(sInstanceMutex);
  if (!sInstance) {
    return LookupResult(MatchType::NOT_FOUND);
  }

  return sInstance->LookupBestMatch(aImageKey, aSurfaceKey, lock);
}

/* static */ InsertOutcome
SurfaceCache::Insert(NotNull<ISurfaceProvider*> aProvider)
{
  StaticMutexAutoLock lock(sInstanceMutex);
  if (!sInstance) {
    return InsertOutcome::FAILURE;
  }

  return sInstance->Insert(aProvider, /* aSetAvailable = */ false, lock);
}

/* static */ bool
SurfaceCache::CanHold(const IntSize& aSize, uint32_t aBytesPerPixel /* = 4 */)
{
  StaticMutexAutoLock lock(sInstanceMutex);
  if (!sInstance) {
    return false;
  }

  Cost cost = ComputeCost(aSize, aBytesPerPixel);
  return sInstance->CanHold(cost);
}

/* static */ bool
SurfaceCache::CanHold(size_t aSize)
{
  StaticMutexAutoLock lock(sInstanceMutex);
  if (!sInstance) {
    return false;
  }

  return sInstance->CanHold(aSize);
}

/* static */ void
SurfaceCache::SurfaceAvailable(NotNull<ISurfaceProvider*> aProvider)
{
  StaticMutexAutoLock lock(sInstanceMutex);
  if (!sInstance) {
    return;
  }

  sInstance->SurfaceAvailable(aProvider, lock);
}

/* static */ void
SurfaceCache::LockImage(const ImageKey aImageKey)
{
  StaticMutexAutoLock lock(sInstanceMutex);
  if (sInstance) {
    return sInstance->LockImage(aImageKey);
  }
}

/* static */ void
SurfaceCache::UnlockImage(const ImageKey aImageKey)
{
  StaticMutexAutoLock lock(sInstanceMutex);
  if (sInstance) {
    return sInstance->UnlockImage(aImageKey, lock);
  }
}

/* static */ void
SurfaceCache::UnlockEntries(const ImageKey aImageKey)
{
  StaticMutexAutoLock lock(sInstanceMutex);
  if (sInstance) {
    return sInstance->UnlockEntries(aImageKey, lock);
  }
}

/* static */ void
SurfaceCache::RemoveImage(const ImageKey aImageKey)
{
  StaticMutexAutoLock lock(sInstanceMutex);
  if (sInstance) {
    sInstance->RemoveImage(aImageKey, lock);
  }
}

/* static */ void
SurfaceCache::DiscardAll()
{
  StaticMutexAutoLock lock(sInstanceMutex);
  if (sInstance) {
    sInstance->DiscardAll(lock);
  }
}

/* static */ void
SurfaceCache::CollectSizeOfSurfaces(const ImageKey                  aImageKey,
                                    nsTArray<SurfaceMemoryCounter>& aCounters,
                                    MallocSizeOf                    aMallocSizeOf)
{
  StaticMutexAutoLock lock(sInstanceMutex);
  if (!sInstance) {
    return;
  }

  return sInstance->CollectSizeOfSurfaces(aImageKey, aCounters, aMallocSizeOf);
}

/* static */ size_t
SurfaceCache::MaximumCapacity()
{
  StaticMutexAutoLock lock(sInstanceMutex);
  if (!sInstance) {
    return 0;
  }

  return sInstance->MaximumCapacity();
}

} // namespace image
} // namespace mozilla