js/src/ds/LifoAlloc.cpp
author Csoregi Natalia <ncsoregi@mozilla.com>
Fri, 25 Sep 2020 10:19:54 +0300
changeset 550277 8734082cd6de033539bb6c5e24709bc9647b5b1a
parent 501264 879358fd177136a5989b627dd297957065c12105
permissions -rw-r--r--
Backed out changeset 78024e47a901 (bug 1657822) for failures on test_bug1303704.html. CLOSED TREE

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 * 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/. */

#include "ds/LifoAlloc.h"

#include "mozilla/Likely.h"
#include "mozilla/MathAlgorithms.h"

#include <algorithm>

#include "ds/MemoryProtectionExceptionHandler.h"

#ifdef LIFO_CHUNK_PROTECT
#  include "gc/Memory.h"
#endif

using namespace js;

using mozilla::RoundUpPow2;
using mozilla::tl::BitSize;

namespace js {
namespace detail {

/* static */
UniquePtr<BumpChunk> BumpChunk::newWithCapacity(size_t size) {
  MOZ_DIAGNOSTIC_ASSERT(size >= sizeof(BumpChunk));
  void* mem = js_malloc(size);
  if (!mem) {
    return nullptr;
  }

  UniquePtr<BumpChunk> result(new (mem) BumpChunk(size));

  // We assume that the alignment of LIFO_ALLOC_ALIGN is less than that of the
  // underlying memory allocator -- creating a new BumpChunk should always
  // satisfy the LIFO_ALLOC_ALIGN alignment constraint.
  MOZ_ASSERT(AlignPtr(result->begin()) == result->begin());
  return result;
}

#ifdef LIFO_CHUNK_PROTECT

static uint8_t* AlignPtrUp(uint8_t* ptr, uintptr_t align) {
  MOZ_ASSERT(mozilla::IsPowerOfTwo(align));
  uintptr_t uptr = uintptr_t(ptr);
  uintptr_t diff = uptr & (align - 1);
  diff = (align - diff) & (align - 1);
  uptr = uptr + diff;
  return (uint8_t*)uptr;
}

static uint8_t* AlignPtrDown(uint8_t* ptr, uintptr_t align) {
  MOZ_ASSERT(mozilla::IsPowerOfTwo(align));
  uintptr_t uptr = uintptr_t(ptr);
  uptr = uptr & ~(align - 1);
  return (uint8_t*)uptr;
}

void BumpChunk::setReadOnly() {
  uintptr_t pageSize = gc::SystemPageSize();
  // The allocated chunks might not be aligned on page boundaries. This code
  // is used to ensure that we are changing the memory protection of pointers
  // which are within the range of the BumpChunk, or that the range formed by
  // [b .. e] is empty.
  uint8_t* b = base();
  uint8_t* e = capacity_;
  b = AlignPtrUp(b, pageSize);
  e = AlignPtrDown(e, pageSize);
  if (e <= b) {
    return;
  }
  js::MemoryProtectionExceptionHandler::addRegion(base(), capacity_ - base());
  gc::MakePagesReadOnly(b, e - b);
}

void BumpChunk::setReadWrite() {
  uintptr_t pageSize = gc::SystemPageSize();
  // The allocated chunks might not be aligned on page boundaries. This code
  // is used to ensure that we are changing the memory protection of pointers
  // which are within the range of the BumpChunk, or that the range formed by
  // [b .. e] is empty.
  uint8_t* b = base();
  uint8_t* e = capacity_;
  b = AlignPtrUp(b, pageSize);
  e = AlignPtrDown(e, pageSize);
  if (e <= b) {
    return;
  }
  gc::UnprotectPages(b, e - b);
  js::MemoryProtectionExceptionHandler::removeRegion(base());
}

#endif

}  // namespace detail
}  // namespace js

void LifoAlloc::reset(size_t defaultChunkSize) {
  MOZ_ASSERT(mozilla::IsPowerOfTwo(defaultChunkSize));

  while (!chunks_.empty()) {
    chunks_.popFirst();
  }
  while (!oversize_.empty()) {
    oversize_.popFirst();
  }
  while (!unused_.empty()) {
    unused_.popFirst();
  }
  defaultChunkSize_ = defaultChunkSize;
  oversizeThreshold_ = defaultChunkSize;
  markCount = 0;
  curSize_ = 0;
  smallAllocsSize_ = 0;
}

void LifoAlloc::freeAll() {
  // When free-ing all chunks, we can no longer determine which chunks were
  // transferred and which were not, so simply clear the heuristic to zero
  // right away.
  smallAllocsSize_ = 0;

  while (!chunks_.empty()) {
    UniqueBumpChunk bc = chunks_.popFirst();
    decrementCurSize(bc->computedSizeOfIncludingThis());
  }
  while (!oversize_.empty()) {
    UniqueBumpChunk bc = oversize_.popFirst();
    decrementCurSize(bc->computedSizeOfIncludingThis());
  }
  while (!unused_.empty()) {
    UniqueBumpChunk bc = unused_.popFirst();
    decrementCurSize(bc->computedSizeOfIncludingThis());
  }

  // Nb: maintaining curSize_ correctly isn't easy.  Fortunately, this is an
  // excellent sanity check.
  MOZ_ASSERT(curSize_ == 0);
}

// Round at the same page granularity used by malloc.
static size_t MallocGoodSize(size_t aSize) {
#if defined(MOZ_MEMORY)
  return malloc_good_size(aSize);
#else
  return aSize;
#endif
}

// Heuristic to choose the size of the next BumpChunk for small allocations.
// `start` is the size of the first chunk. `used` is the total size of all
// BumpChunks in this LifoAlloc so far.
static size_t NextSize(size_t start, size_t used) {
  // Double the size, up to 1 MB.
  const size_t mb = 1 * 1024 * 1024;
  if (used < mb) {
    return std::max(start, used);
  }

  // After 1 MB, grow more gradually, to waste less memory.
  // The sequence (in megabytes) begins:
  // 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5, ...
  return RoundUp(used / 8, mb);
}

LifoAlloc::UniqueBumpChunk LifoAlloc::newChunkWithCapacity(size_t n,
                                                           bool oversize) {
  MOZ_ASSERT(fallibleScope_,
             "[OOM] Cannot allocate a new chunk in an infallible scope.");

  // Compute the size which should be requested in order to be able to fit |n|
  // bytes in a newly allocated chunk, or default to |defaultChunkSize_|.

  size_t minSize;
  if (MOZ_UNLIKELY(!detail::BumpChunk::allocSizeWithRedZone(n, &minSize) ||
                   (minSize & (size_t(1) << (BitSize<size_t>::value - 1))))) {
    return nullptr;
  }

  // Note: When computing chunkSize growth, we only are interested in chunks
  // used for small allocations. This excludes unused chunks, oversized chunks,
  // and chunks transferred in from another LifoAlloc.
  MOZ_ASSERT(curSize_ >= smallAllocsSize_);
  const size_t chunkSize = (oversize || minSize > defaultChunkSize_)
                               ? MallocGoodSize(minSize)
                               : NextSize(defaultChunkSize_, smallAllocsSize_);

  // Create a new BumpChunk, and allocate space for it.
  UniqueBumpChunk result = detail::BumpChunk::newWithCapacity(chunkSize);
  if (!result) {
    return nullptr;
  }
  MOZ_ASSERT(result->computedSizeOfIncludingThis() == chunkSize);
  return result;
}

LifoAlloc::UniqueBumpChunk LifoAlloc::getOrCreateChunk(size_t n) {
  // Look for existing unused BumpChunks to satisfy the request, and pick the
  // first one which is large enough, and move it into the list of used
  // chunks.
  if (!unused_.empty()) {
    if (unused_.begin()->canAlloc(n)) {
      return unused_.popFirst();
    }

    BumpChunkList::Iterator e(unused_.end());
    for (BumpChunkList::Iterator i(unused_.begin()); i->next() != e.get();
         ++i) {
      detail::BumpChunk* elem = i->next();
      MOZ_ASSERT(elem->empty());
      if (elem->canAlloc(n)) {
        BumpChunkList temp = unused_.splitAfter(i.get());
        UniqueBumpChunk newChunk = temp.popFirst();
        unused_.appendAll(std::move(temp));
        return newChunk;
      }
    }
  }

  // Allocate a new BumpChunk with enough space for the next allocation.
  UniqueBumpChunk newChunk = newChunkWithCapacity(n, false);
  if (!newChunk) {
    return newChunk;
  }
  incrementCurSize(newChunk->computedSizeOfIncludingThis());
  return newChunk;
}

void* LifoAlloc::allocImplColdPath(size_t n) {
  void* result;
  UniqueBumpChunk newChunk = getOrCreateChunk(n);
  if (!newChunk) {
    return nullptr;
  }

  // This new chunk is about to be used for small allocations.
  smallAllocsSize_ += newChunk->computedSizeOfIncludingThis();

  // Since we just created a large enough chunk, this can't fail.
  chunks_.append(std::move(newChunk));
  result = chunks_.last()->tryAlloc(n);
  MOZ_ASSERT(result);
  return result;
}

void* LifoAlloc::allocImplOversize(size_t n) {
  void* result;
  UniqueBumpChunk newChunk = newChunkWithCapacity(n, true);
  if (!newChunk) {
    return nullptr;
  }
  incrementCurSize(newChunk->computedSizeOfIncludingThis());

  // Since we just created a large enough chunk, this can't fail.
  oversize_.append(std::move(newChunk));
  result = oversize_.last()->tryAlloc(n);
  MOZ_ASSERT(result);
  return result;
}

bool LifoAlloc::ensureUnusedApproximateColdPath(size_t n, size_t total) {
  for (detail::BumpChunk& bc : unused_) {
    total += bc.unused();
    if (total >= n) {
      return true;
    }
  }

  UniqueBumpChunk newChunk = newChunkWithCapacity(n, false);
  if (!newChunk) {
    return false;
  }
  incrementCurSize(newChunk->computedSizeOfIncludingThis());
  unused_.pushFront(std::move(newChunk));
  return true;
}

LifoAlloc::Mark LifoAlloc::mark() {
  markCount++;
  Mark res;
  if (!chunks_.empty()) {
    res.chunk = chunks_.last()->mark();
  }
  if (!oversize_.empty()) {
    res.oversize = oversize_.last()->mark();
  }
  return res;
}

void LifoAlloc::release(Mark mark) {
  markCount--;
#ifdef DEBUG
  auto assertIsContained = [](const detail::BumpChunk::Mark& m,
                              BumpChunkList& list) {
    if (m.markedChunk()) {
      bool contained = false;
      for (const detail::BumpChunk& chunk : list) {
        if (&chunk == m.markedChunk() && chunk.contains(m)) {
          contained = true;
          break;
        }
      }
      MOZ_ASSERT(contained);
    }
  };
  assertIsContained(mark.chunk, chunks_);
  assertIsContained(mark.oversize, oversize_);
#endif

  BumpChunkList released;
  auto cutAtMark = [&released](const detail::BumpChunk::Mark& m,
                               BumpChunkList& list) {
    // Move the blocks which are after the mark to the set released chunks.
    if (!m.markedChunk()) {
      released = std::move(list);
    } else {
      released = list.splitAfter(m.markedChunk());
    }

    // Release everything which follows the mark in the last chunk.
    if (!list.empty()) {
      list.last()->release(m);
    }
  };

  // Release the content of all the blocks which are after the marks, and keep
  // blocks as unused.
  cutAtMark(mark.chunk, chunks_);
  for (detail::BumpChunk& bc : released) {
    bc.release();

    // Chunks moved from (after a mark) in chunks_ to unused_ are no longer
    // considered small allocations.
    smallAllocsSize_ -= bc.computedSizeOfIncludingThis();
  }
  unused_.appendAll(std::move(released));

  // Free the content of all the blocks which are after the marks.
  cutAtMark(mark.oversize, oversize_);
  while (!released.empty()) {
    UniqueBumpChunk bc = released.popFirst();
    decrementCurSize(bc->computedSizeOfIncludingThis());
  }
}

void LifoAlloc::steal(LifoAlloc* other) {
  MOZ_ASSERT(!other->markCount);
  MOZ_DIAGNOSTIC_ASSERT(unused_.empty());
  MOZ_DIAGNOSTIC_ASSERT(chunks_.empty());
  MOZ_DIAGNOSTIC_ASSERT(oversize_.empty());

  // Copy everything from |other| to |this| except for |peakSize_|, which
  // requires some care.
  chunks_ = std::move(other->chunks_);
  oversize_ = std::move(other->oversize_);
  unused_ = std::move(other->unused_);
  markCount = other->markCount;
  defaultChunkSize_ = other->defaultChunkSize_;
  oversizeThreshold_ = other->oversizeThreshold_;
  curSize_ = other->curSize_;
  peakSize_ = std::max(peakSize_, other->peakSize_);
  smallAllocsSize_ = other->smallAllocsSize_;
#if defined(DEBUG) || defined(JS_OOM_BREAKPOINT)
  fallibleScope_ = other->fallibleScope_;
#endif

  other->reset(defaultChunkSize_);
}

void LifoAlloc::transferFrom(LifoAlloc* other) {
  MOZ_ASSERT(!markCount);
  MOZ_ASSERT(!other->markCount);

  // Transferred chunks are not counted as part of |smallAllocsSize| as this
  // could introduce bias in the |NextSize| heuristics, leading to
  // over-allocations in *this* LifoAlloc. As well, to avoid interference with
  // small allocations made with |this|, the last chunk of the |chunks_| list
  // should remain the last chunk. Therefore, the transferred chunks are
  // prepended to the |chunks_| list.
  incrementCurSize(other->curSize_);

  appendUnused(std::move(other->unused_));
  chunks_.prependAll(std::move(other->chunks_));
  oversize_.prependAll(std::move(other->oversize_));
  other->curSize_ = 0;
  other->smallAllocsSize_ = 0;
}

void LifoAlloc::transferUnusedFrom(LifoAlloc* other) {
  MOZ_ASSERT(!markCount);

  size_t size = 0;
  for (detail::BumpChunk& bc : other->unused_) {
    size += bc.computedSizeOfIncludingThis();
  }

  appendUnused(std::move(other->unused_));
  incrementCurSize(size);
  other->decrementCurSize(size);
}

#ifdef LIFO_CHUNK_PROTECT
void LifoAlloc::setReadOnly() {
  for (detail::BumpChunk& bc : chunks_) {
    bc.setReadOnly();
  }
  for (detail::BumpChunk& bc : oversize_) {
    bc.setReadOnly();
  }
  for (detail::BumpChunk& bc : unused_) {
    bc.setReadOnly();
  }
}

void LifoAlloc::setReadWrite() {
  for (detail::BumpChunk& bc : chunks_) {
    bc.setReadWrite();
  }
  for (detail::BumpChunk& bc : oversize_) {
    bc.setReadWrite();
  }
  for (detail::BumpChunk& bc : unused_) {
    bc.setReadWrite();
  }
}
#endif