Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/mozglue/baseprofiler/public/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 21 kB image not shown  

Quelle  ProgressLogger.h   Sprache: C

 
/* -*- 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/. */


#ifndef ProgressLogger_h
#define ProgressLogger_h

#include "mozilla/Assertions.h"
#include "mozilla/ProportionValue.h"
#include "mozilla/RefCounted.h"
#include "mozilla/RefPtr.h"

#include <atomic>

// Uncomment to printf ProcessLogger updates.
// #define DEBUG_PROCESSLOGGER

#ifdef DEBUG_PROCESSLOGGER
#  include "mozilla/BaseProfilerUtils.h"
#  include <cstdio>
#endif  // DEBUG_PROCESSLOGGER

namespace mozilla {

// A `ProgressLogger` is used to update a referenced atomic `ProportionValue`,
// and can recursively create a sub-logger corresponding to a subset of their
// own range, but that sub-logger's updates are done in its local 0%-100% range.
// The typical usage is for multi-level tasks, where each level can estimate its
// own work and the work delegated to a next-level function, without knowing how
// this local work relates to the higher-level total work. See
// `CreateSubLoggerFromTo` for details.
// Note that this implementation is single-threaded, it does not support logging
// progress from multiple threads at the same time.
class ProgressLogger {
 public:
  // An RefPtr'd object of this class is used as the target of all
  // ProgressLogger updates, and it may be shared to make these updates visible
  // from other code in any thread.
  class SharedProgress : public external::AtomicRefCounted<SharedProgress> {
   public:
    MOZ_DECLARE_REFCOUNTED_TYPENAME(SharedProgress)

    SharedProgress() = default;

    SharedProgress(const SharedProgress&) = delete;
    SharedProgress& operator=(const SharedProgress&) = delete;

    // This constant is used to indicate that an update may change the progress
    // value, but should not modify the previously-recorded location.
    static constexpr const char* NO_LOCATION_UPDATE = nullptr;

    // Set the current progress and location, but the previous location is not
    // overwritten if the new one is null or empty.
    // The location and then the progress are atomically "released", so that all
    // preceding writes on this thread will be visible to other threads reading
    // these values; most importantly when reaching 100% progress, the reader
    // can be confident that the location is final and the operation being
    // watched has completed.
    void SetProgress(
        ProportionValue aProgress,
        const char* aLocationOrNullEmptyToIgnore = NO_LOCATION_UPDATE) {
      if (aLocationOrNullEmptyToIgnore &&
          *aLocationOrNullEmptyToIgnore != '\0') {
        mLastLocation.store(aLocationOrNullEmptyToIgnore,
                            std::memory_order_release);
      }
      mProgress.store(aProgress, std::memory_order_release);
    }

    // Read the current progress value. Atomically "acquired", so that writes
    // from the thread that stored this value are all visible to the reader
    // here; most importantly when reaching 100%, we can be confident that the
    // location is final and the operation being watched has completed.
    [[nodiscard]] ProportionValue Progress() const {
      return mProgress.load(std::memory_order_acquire);
    }

    // Read the current progress value. Atomically "acquired".
    [[nodiscard]] const char* LastLocation() const {
      return mLastLocation.load(std::memory_order_acquire);
    }

   private:
    friend mozilla::detail::RefCounted<SharedProgress,
                                       mozilla::detail::AtomicRefCount>;
    ~SharedProgress() = default;

    // Progress and last-known location.
    // Beware that these two values are not strongly tied: Reading one then the
    // other may give mismatched information; but it should be fine for
    // informational usage.
    // They are stored using atomic acquire-release ordering, to guarantee that
    // when read, all writes preceding these values are visible.
    std::atomic<ProportionValue> mProgress = ProportionValue{0.0};
    std::atomic<const char*> mLastLocation = nullptr;
  };

  static constexpr const char* NO_LOCATION_UPDATE =
      SharedProgress::NO_LOCATION_UPDATE;

  ProgressLogger() = default;

  // Construct a top-level logger, starting at 0% and expected to end at 100%.
  explicit ProgressLogger(
      RefPtr<SharedProgress> aGlobalProgressOrNull,
      const char* aLocationOrNullEmptyToIgnoreAtStart = NO_LOCATION_UPDATE,
      const char* aLocationOrNullEmptyToIgnoreAtEnd = NO_LOCATION_UPDATE)
      : ProgressLogger{std::move(aGlobalProgressOrNull),
                       /* Start */ ProportionValue{0.0},
                       /* Multiplier */ ProportionValue{1.0},
                       aLocationOrNullEmptyToIgnoreAtStart,
                       aLocationOrNullEmptyToIgnoreAtEnd} {}

  // Don't make copies, it would be confusing!
  // TODO: Copies could one day be allowed to track multi-threaded work, but it
  // is outside the scope of this implementation; Please update if needed.
  ProgressLogger(const ProgressLogger&) = delete;
  ProgressLogger& operator&(const ProgressLogger&) = delete;

  // Move-construct is allowed, to return from CreateSubLoggerFromTo, and
  // forward straight into a function. Note that moved-from ProgressLoggers must
  // not be used anymore! Use `CreateSubLoggerFromTo` to pass a sub-logger to
  // functions.
  ProgressLogger(ProgressLogger&& aOther)
      : mGlobalProgressOrNull(std::move(aOther.mGlobalProgressOrNull)),
        mLocalStartInGlobalSpace(aOther.mLocalStartInGlobalSpace),
        mLocalToGlobalMultiplier(aOther.mLocalToGlobalMultiplier),
        mLocationAtDestruction(aOther.mLocationAtDestruction) {
    aOther.MarkMovedFrom();
#ifdef DEBUG_PROCESSLOGGER
    if (mGlobalProgressOrNull) {
      printf("[%d] Moved (staying globally at %.2f in [%.2f, %.2f])\n",
             int(baseprofiler::profiler_current_process_id().ToNumber()),
             GetGlobalProgress().ToDouble() * 100.0,
             mLocalStartInGlobalSpace.ToDouble() * 100.0,
             (mLocalStartInGlobalSpace + mLocalToGlobalMultiplier).ToDouble() *
                 100.0);
    }
#endif  // DEBUG_PROCESSLOGGER
  }

  // Move-assign. This may be useful when starting with a default (empty) logger
  // and later assigning it a progress value to start updating.
  ProgressLogger& operator=(ProgressLogger&& aOther) {
    mGlobalProgressOrNull = std::move(aOther.mGlobalProgressOrNull);
    mLocalStartInGlobalSpace = aOther.mLocalStartInGlobalSpace;
    mLocalToGlobalMultiplier = aOther.mLocalToGlobalMultiplier;
    mLocationAtDestruction = aOther.mLocationAtDestruction;
    aOther.MarkMovedFrom();
#ifdef DEBUG_PROCESSLOGGER
    if (mGlobalProgressOrNull) {
      printf("[%d] Re-assigned (globally at %.2f in [%.2f, %.2f])\n",
             int(baseprofiler::profiler_current_process_id().ToNumber()),
             GetGlobalProgress().ToDouble() * 100.0,
             mLocalStartInGlobalSpace.ToDouble() * 100.0,
             (mLocalStartInGlobalSpace + mLocalToGlobalMultiplier).ToDouble() *
                 100.0);
    }
#endif  // DEBUG_PROCESSLOGGER
    return *this;
  }

  // Destruction sets the local update value to 100% unless empty or moved-from.
  ~ProgressLogger() {
    if (!IsMovedFrom()) {
#ifdef DEBUG_PROCESSLOGGER
      if (mGlobalProgressOrNull) {
        printf("[%d] Destruction:\n",
               int(baseprofiler::profiler_current_process_id().ToNumber()));
      }
#endif  // DEBUG_PROCESSLOGGER
      SetLocalProgress(ProportionValue{1.0}, mLocationAtDestruction);
    }
  }

  // Retrieve the current progress in the global space. May be invalid.
  [[nodiscard]] ProportionValue GetGlobalProgress() const {
    return mGlobalProgressOrNull ? mGlobalProgressOrNull->Progress()
                                 : ProportionValue::MakeInvalid();
  }

  // Retrieve the last known global location. May be null.
  [[nodiscard]] const char* GetLastGlobalLocation() const {
    return mGlobalProgressOrNull ? mGlobalProgressOrNull->LastLocation()
                                 : nullptr;
  }

  // Set the current progress in the local space.
  void SetLocalProgress(ProportionValue aLocalProgress,
                        const char* aLocationOrNullEmptyToIgnore) {
    MOZ_ASSERT(!IsMovedFrom());
    if (mGlobalProgressOrNull && !mLocalToGlobalMultiplier.IsExactlyZero()) {
      mGlobalProgressOrNull->SetProgress(LocalToGlobal(aLocalProgress),
                                         aLocationOrNullEmptyToIgnore);
#ifdef DEBUG_PROCESSLOGGER
      printf("[%d] - local %.0f%% ~ global %.2f%% \"%s\"\n",
             int(baseprofiler::profiler_current_process_id().ToNumber()),
             aLocalProgress.ToDouble() * 100.0,
             LocalToGlobal(aLocalProgress).ToDouble() * 100.0,
             aLocationOrNullEmptyToIgnore ? aLocationOrNullEmptyToIgnore
                                          : "");
#endif  // DEBUG_PROCESSLOGGER
    }
  }

  // Create a sub-logger that will record progress in the given local range.
  // E.g.: `f(pl.CreateSubLoggerFromTo(0.2, "f...", 0.4, "f done"));` expects
  // that `f` will produce work in the local range 0.2 (when starting) to 0.4
  // (when returning); `f` itself will update this provided logger from 0.0
  // to 1.0 (local to that `f` function), which will effectively be converted to
  // 0.2-0.4 (local to the calling function).
  // This can cascade multiple levels, each deeper level affecting a smaller and
  // smaller range in the global output.
  [[nodiscard]] ProgressLogger CreateSubLoggerFromTo(
      ProportionValue aSubStartInLocalSpace,
      const char* aLocationOrNullEmptyToIgnoreAtStart,
      ProportionValue aSubEndInLocalSpace,
      const char* aLocationOrNullEmptyToIgnoreAtEnd = NO_LOCATION_UPDATE) {
    MOZ_ASSERT(!IsMovedFrom());
    if (!mGlobalProgressOrNull) {
      return ProgressLogger{};
    }
    const ProportionValue subStartInGlobalSpace =
        LocalToGlobal(aSubStartInLocalSpace);
    const ProportionValue subEndInGlobalSpace =
        LocalToGlobal(aSubEndInLocalSpace);
    if (subStartInGlobalSpace.IsInvalid() || subEndInGlobalSpace.IsInvalid()) {
      return ProgressLogger{mGlobalProgressOrNull,
                            /* Start */ ProportionValue::MakeInvalid(),
                            /* Multiplier */ ProportionValue{0.0},
                            aLocationOrNullEmptyToIgnoreAtStart,
                            aLocationOrNullEmptyToIgnoreAtEnd};
    }
#ifdef DEBUG_PROCESSLOGGER
    if (mGlobalProgressOrNull) {
      printf("[%d] * Sub: local [%.0f%%, %.0f%%] ~ global [%.2f%%, %.2f%%]\n",
             int(baseprofiler::profiler_current_process_id().ToNumber()),
             aSubStartInLocalSpace.ToDouble() * 100.0,
             aSubEndInLocalSpace.ToDouble() * 100.0,
             subStartInGlobalSpace.ToDouble() * 100.0,
             subEndInGlobalSpace.ToDouble() * 100.0);
    }
#endif  // DEBUG_PROCESSLOGGER
    return ProgressLogger{
        mGlobalProgressOrNull,
        /* Start */ subStartInGlobalSpace,
        /* Multipler */ subEndInGlobalSpace - subStartInGlobalSpace,
        aLocationOrNullEmptyToIgnoreAtStart, aLocationOrNullEmptyToIgnoreAtEnd};
  }

  // Helper with no start location.
  [[nodiscard]] ProgressLogger CreateSubLoggerFromTo(
      ProportionValue aSubStartInLocalSpace,
      ProportionValue aSubEndInLocalSpace,
      const char* aLocationOrNullEmptyToIgnoreAtEnd = NO_LOCATION_UPDATE) {
    return CreateSubLoggerFromTo(aSubStartInLocalSpace, NO_LOCATION_UPDATE,
                                 aSubEndInLocalSpace,
                                 aLocationOrNullEmptyToIgnoreAtEnd);
  }

  // Helper using the current progress as start.
  [[nodiscard]] ProgressLogger CreateSubLoggerTo(
      const char* aLocationOrNullEmptyToIgnoreAtStart,
      ProportionValue aSubEndInLocalSpace,
      const char* aLocationOrNullEmptyToIgnoreAtEnd = NO_LOCATION_UPDATE) {
    MOZ_ASSERT(!IsMovedFrom());
    if (!mGlobalProgressOrNull) {
      return ProgressLogger{};
    }
    const ProportionValue subStartInGlobalSpace = GetGlobalProgress();
    const ProportionValue subEndInGlobalSpace =
        LocalToGlobal(aSubEndInLocalSpace);
    if (subStartInGlobalSpace.IsInvalid() || subEndInGlobalSpace.IsInvalid()) {
      return ProgressLogger{mGlobalProgressOrNull,
                            /* Start */ ProportionValue::MakeInvalid(),
                            /* Multiplier */ ProportionValue{0.0},
                            aLocationOrNullEmptyToIgnoreAtStart,
                            aLocationOrNullEmptyToIgnoreAtEnd};
    }
#ifdef DEBUG_PROCESSLOGGER
    if (mGlobalProgressOrNull) {
      printf("[%d] * Sub: local [(here), %.0f%%] ~ global [%.2f%%, %.2f%%]\n",
             int(baseprofiler::profiler_current_process_id().ToNumber()),
             aSubEndInLocalSpace.ToDouble() * 100.0,
             subStartInGlobalSpace.ToDouble() * 100.0,
             subEndInGlobalSpace.ToDouble() * 100.0);
    }
#endif  // DEBUG_PROCESSLOGGER
    return ProgressLogger{
        mGlobalProgressOrNull,
        /* Start */ subStartInGlobalSpace,
        /* Multiplier */ subEndInGlobalSpace - subStartInGlobalSpace,
        aLocationOrNullEmptyToIgnoreAtStart, aLocationOrNullEmptyToIgnoreAtEnd};
  }

  // Helper using the current progress as start, no start location.
  [[nodiscard]] ProgressLogger CreateSubLoggerTo(
      ProportionValue aSubEndInLocalSpace,
      const char* aLocationOrNullEmptyToIgnoreAtEnd = NO_LOCATION_UPDATE) {
    return CreateSubLoggerTo(NO_LOCATION_UPDATE, aSubEndInLocalSpace,
                             aLocationOrNullEmptyToIgnoreAtEnd);
  }

  class IndexAndProgressLoggerRange;

  [[nodiscard]] inline IndexAndProgressLoggerRange CreateLoopSubLoggersFromTo(
      ProportionValue aLoopStartInLocalSpace,
      ProportionValue aLoopEndInLocalSpace, uint32_t aLoopCount,
      const char* aLocationOrNullEmptyToIgnoreAtEdges =
          ProgressLogger::NO_LOCATION_UPDATE);
  [[nodiscard]] inline IndexAndProgressLoggerRange CreateLoopSubLoggersTo(
      ProportionValue aLoopEndInLocalSpace, uint32_t aLoopCount,
      const char* aLocationOrNullEmptyToIgnoreAtEdges =
          ProgressLogger::NO_LOCATION_UPDATE);

 private:
  // All constructions start at the local 0%.
  ProgressLogger(RefPtr<SharedProgress> aGlobalProgressOrNull,
                 ProportionValue aLocalStartInGlobalSpace,
                 ProportionValue aLocalToGlobalMultiplier,
                 const char* aLocationOrNullEmptyToIgnoreAtConstruction,
                 const char* aLocationOrNullEmptyToIgnoreAtDestruction)
      : mGlobalProgressOrNull(std::move(aGlobalProgressOrNull)),
        mLocalStartInGlobalSpace(aLocalStartInGlobalSpace),
        mLocalToGlobalMultiplier(aLocalToGlobalMultiplier),
        mLocationAtDestruction(aLocationOrNullEmptyToIgnoreAtDestruction) {
    MOZ_ASSERT(!IsMovedFrom(), "Don't construct a moved-from object!");
    SetLocalProgress(ProportionValue{0.0},
                     aLocationOrNullEmptyToIgnoreAtConstruction);
  }

  void MarkMovedFrom() {
    mLocalToGlobalMultiplier = ProportionValue::MakeInvalid();
  }
  [[nodiscard]] bool IsMovedFrom() const {
    return mLocalToGlobalMultiplier.IsInvalid();
  }

  [[nodiscard]] ProportionValue LocalToGlobal(
      ProportionValue aLocalProgress) const {
    return aLocalProgress * mLocalToGlobalMultiplier + mLocalStartInGlobalSpace;
  }

  // Global progress value to update from local changes.
  RefPtr<SharedProgress> mGlobalProgressOrNull;

  // How much to multiply and add to a local [0, 100%] value, to get the
  // corresponding value in the global space.
  // If mLocalToGlobalMultiplier is invalid, this ProgressLogger is moved-from,
  // functions should not be used, and destructor won't update progress.
  ProportionValue mLocalStartInGlobalSpace;
  ProportionValue mLocalToGlobalMultiplier;

  const char* mLocationAtDestruction = nullptr;
};

// Helper class for range-for loop, e.g., with `aProgressLogger`:
//   for (auto [index, loopProgressLogger] :
//        IndexAndProgressLoggerRange{aProgressLogger, 30_pc, 50_pc, 10,
//                                    "looping..."}) {
//     // This will loop 10 times.
//     // `index` is the loop index, from 0 to 9.
//     // The overall loop will start at 30% and end at 50% of aProgressLogger.
//     // `loopProgressLogger` is the progress logger for each iteration,
//     // covering 1/10th of the range, therefore: [30%,32%], then [32%,34%],
//     // etc. until [48%,50%].
//     // Progress is automatically updated before/after each loop.
//   }
// Note that this implementation is single-threaded, it does not support logging
// progress from parallel loops.
class ProgressLogger::IndexAndProgressLoggerRange {
 public:
  struct IndexAndProgressLogger {
    uint32_t index;
    ProgressLogger progressLogger;
  };

  class IndexAndProgressLoggerEndIterator {
   public:
    explicit IndexAndProgressLoggerEndIterator(uint32_t aIndex)
        : mIndex(aIndex) {}

    [[nodiscard]] uint32_t Index() const { return mIndex; }

   private:
    uint32_t mIndex;
  };

  class IndexAndProgressLoggerIterator {
   public:
    IndexAndProgressLoggerIterator(
        RefPtr<ProgressLogger::SharedProgress> aGlobalProgressOrNull,
        ProportionValue aLoopStartInGlobalSpace,
        ProportionValue aLoopIncrementInGlobalSpace,
        const char* aLocationOrNullEmptyToIgnoreAtEdges)
        : mGlobalProgressOrNull(aGlobalProgressOrNull),
          mLoopStartInGlobalSpace(aLoopStartInGlobalSpace),
          mLoopIncrementInGlobalSpace(aLoopIncrementInGlobalSpace),
          mIndex(0u),
          mLocationOrNullEmptyToIgnoreAtEdges(
              aLocationOrNullEmptyToIgnoreAtEdges) {
      if (mGlobalProgressOrNull) {
        mGlobalProgressOrNull->SetProgress(mLoopStartInGlobalSpace,
                                           mLocationOrNullEmptyToIgnoreAtEdges);
      }
    }

    [[nodiscard]] IndexAndProgressLogger operator*() {
      return IndexAndProgressLogger{
          mIndex,
          mGlobalProgressOrNull
              ? ProgressLogger{mGlobalProgressOrNull, mLoopStartInGlobalSpace,
                               mLoopIncrementInGlobalSpace,
                               ProgressLogger::NO_LOCATION_UPDATE,
                               ProgressLogger::NO_LOCATION_UPDATE}
              : ProgressLogger{}};
    }

    [[nodiscard]] bool operator!=(
        const IndexAndProgressLoggerEndIterator& aEnd) const {
      return mIndex != aEnd.Index();
    }

    IndexAndProgressLoggerIterator& operator++() {
      ++mIndex;
      mLoopStartInGlobalSpace =
          mLoopStartInGlobalSpace + mLoopIncrementInGlobalSpace;
      if (mGlobalProgressOrNull) {
        mGlobalProgressOrNull->SetProgress(mLoopStartInGlobalSpace,
                                           mLocationOrNullEmptyToIgnoreAtEdges);
      }
      return *this;
    }

   private:
    RefPtr<ProgressLogger::SharedProgress> mGlobalProgressOrNull;
    ProportionValue mLoopStartInGlobalSpace;
    ProportionValue mLoopIncrementInGlobalSpace;
    uint32_t mIndex;
    const char* mLocationOrNullEmptyToIgnoreAtEdges;
  };

  [[nodiscard]] IndexAndProgressLoggerIterator begin() {
    return IndexAndProgressLoggerIterator{
        mGlobalProgressOrNull, mLoopStartInGlobalSpace,
        mLoopIncrementInGlobalSpace, mLocationOrNullEmptyToIgnoreAtEdges};
  }

  [[nodiscard]] IndexAndProgressLoggerEndIterator end() {
    return IndexAndProgressLoggerEndIterator{mLoopCount};
  }

 private:
  friend class ProgressLogger;
  IndexAndProgressLoggerRange(ProgressLogger& aProgressLogger,
                              ProportionValue aLoopStartInGlobalSpace,
                              ProportionValue aLoopEndInGlobalSpace,
                              uint32_t aLoopCount,
                              const char* aLocationOrNullEmptyToIgnoreAtEdges =
                                  ProgressLogger::NO_LOCATION_UPDATE)
      : mGlobalProgressOrNull(aProgressLogger.mGlobalProgressOrNull),
        mLoopStartInGlobalSpace(aLoopStartInGlobalSpace),
        mLoopIncrementInGlobalSpace(
            (aLoopEndInGlobalSpace - aLoopStartInGlobalSpace) / aLoopCount),
        mLoopCount(aLoopCount),
        mLocationOrNullEmptyToIgnoreAtEdges(
            aLocationOrNullEmptyToIgnoreAtEdges) {}

  RefPtr<ProgressLogger::SharedProgress> mGlobalProgressOrNull;
  ProportionValue mLoopStartInGlobalSpace;
  ProportionValue mLoopIncrementInGlobalSpace;
  uint32_t mLoopCount;
  const char* mLocationOrNullEmptyToIgnoreAtEdges;
};

[[nodiscard]] ProgressLogger::IndexAndProgressLoggerRange
ProgressLogger::CreateLoopSubLoggersFromTo(
    ProportionValue aLoopStartInLocalSpace,
    ProportionValue aLoopEndInLocalSpace, uint32_t aLoopCount,
    const char* aLocationOrNullEmptyToIgnoreAtEdges) {
  return IndexAndProgressLoggerRange{
      *this, LocalToGlobal(aLoopStartInLocalSpace),
      LocalToGlobal(aLoopEndInLocalSpace), aLoopCount,
      aLocationOrNullEmptyToIgnoreAtEdges};
}

[[nodiscard]] ProgressLogger::IndexAndProgressLoggerRange
ProgressLogger::CreateLoopSubLoggersTo(
    ProportionValue aLoopEndInLocalSpace, uint32_t aLoopCount,
    const char* aLocationOrNullEmptyToIgnoreAtEdges) {
  return IndexAndProgressLoggerRange{
      *this, GetGlobalProgress(), LocalToGlobal(aLoopEndInLocalSpace),
      aLoopCount, aLocationOrNullEmptyToIgnoreAtEdges};
}

}  // namespace mozilla

#endif  // ProgressLogger_h

Messung V0.5
C=93 H=94 G=93

¤ Dauer der Verarbeitung: 0.15 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.