[Userscript] The GanbarOmeter

Please update to v0.6 (just posted) and try again.

I’m no javascript expert and the whole dynamic typing thing is just breaking me. Date objects in particular are just weird: it’s hard to know what’s a number and whats a Date, making math unnecessarily difficult, IMO.

Anyway, I THINK the source of the problem may have been timezone issues coupled with how I was using the Date objects. I was definitely doing something truly weird with the options I was passing to wkof to fetch the review objects. I’ve simplified that significantly.

Not sure if it will fix the problem, but here’s hoping.

If you and @tahubulat could both try version v1.6 and report back, I’d appreciate it.

I’ve updated the script to v0.6 and restarted Firefox, but it’s still stuck on loading. :frowning:

Well that is certainly not alarming at all :sweat_smile:

1 Like

If you have a few minutes, could you load and try this debug version? It adds a few console logs then starts the debugger just before doing the fetch. It won’t actually perform the fetch, it will stop and grey out the screen when the debugger is launched.

YOU WILL NEED TO DISABLE THE REGULAR VERSION BEFORE RUNNING THIS ONE.

Please send me the console output at that point.

Here’s the ganbarometer-debug script;

[My working assumption is that it has something to do with us being in different timezones, and some sort of bug in how I’m calculating Date offsets. I’m mystified why that would be the case, though: it works correctly in this timezone.]


Ganbarometer fetch options: Ganbarometer-debug.user.js:410:13
Mon Sep 20 2021 10:26:24 GMT+0700 (Indochina Time) Ganbarometer-debug.user.js:411:13
Object { last_update: Date Fri Sep 17 2021 10:26:24 GMT+0700 (Indochina Time) }
​
last_update: Date Fri Sep 17 2021 10:26:24 GMT+0700 (Indochina Time)
​
<prototype>: Object { … }
​​
__defineGetter__: function __defineGetter__()
​​
__defineSetter__: function __defineSetter__()
​​
__lookupGetter__: function __lookupGetter__()
​​
__lookupSetter__: function __lookupSetter__()
​​
__proto__: 
​​
constructor: function Object()
​​
hasOwnProperty: function hasOwnProperty()
​​
isPrototypeOf: function isPrototypeOf()
​​
propertyIsEnumerable: function propertyIsEnumerable()
​​
toLocaleString: function toLocaleString()
​​
toString: function toString()
​​
valueOf: function valueOf()
​​
<get __proto__()>: function __proto__()
​​
<set __proto__()>: function __proto__()
Ganbarometer-debug.user.js:412:13
Error: Promised response from onMessage listener went out of scope common.js:4:491

​


edit: the yellow progress bar’s still loading, even after I restart firefox. Is this expected behavior?

You may be comfortable with more apprentice items and a higher percentage of misses than the defaults are set up for.

For “straight up” green indicators the defaults expect:

Difficulty

  1. ~100 Apprentice items (you have 184).

  2. No more than 20% of incorrect answers per day on average. You are getting reviewing about 204 per day on average, with incorrect answers for about 50 (roughly 25%).

  3. Few kanji in stages 1 or 2 (these are weighted more heavily). You only have one at the moment.

Load

150 reviews/day on average. You are performing about 24.

Speed

15 seconds per review on average. You are currently seeing 56 (but I may very well have a bug in the calculations for this gauge – working on it).


Suggestions:

Since your comfort level is well beyond my own, you might want to play with the settings. Try these:

Session interval: 2
Desired apprentice quantity: 180

Please let me know if the shorter session interval lowers your speed significantly. It would mean I counted multiple review sessions as a single session.

That looks absolutely correct, darn it!

It is definitely requesting reviews since Fri 9/17 at 10:26 am (which is 72 hours before the current time).

Please hit the continue button for the debugger and let it run to completion (it may take a while).

Do you have an unusually slow or high-latency internet connection? I’m wondering if it’s just taking a long time to return the three days worth of data (should only be a couple of thousand items at most).

would this be it? sorry, I’m kinda slow with tech :stuck_out_tongue:
Screen Shot 2021-09-20 at 10.37.42

I don’t think my internet is that slow. I live in Bangkok, but my internet is fairly good…

Yes, just click the triangle (play) button.

Since the options to the fetch command are correct, it may be something else entirely (not the fetch) that is taking so long.

ah, the meters are loaded! here’s the debug output:

(function(that){((context, fapply, console) => {with (context) {(module => {"use strict";try {fapply(module, context, [,,context.CDATA,context.uneval,context.define,context.module,context.exports,context.GM,context.GM_info]);} catch (e) {if (e.message && e.stack) {console.error("ERROR: Execution of script 'Ganbarometer-debug' failed! " + e.message);console.log(e.stack.replace(/(\\(eval at )?<anonymous>[: ]?)|([\s.]*at Object.tms_[\s\S.]*)/g, ""));} else {console.error(e);}}})(async function (context,fapply,CDATA,uneval,define,module,exports,GM,GM_info) {
// ==UserScript==
// @name         Wanikani: Review Cache
// @version      1.0.11
// @description  Manages a cache of all the user's reviews
// @author       Kumirei
// ==/UserScript==

(function(wkof) {
    // Manually increment to initiate reload for all users
    const cache_version = 1

    // Reveal functions to window
    if (!window.review_cache || !window.review_cache.version || window.review_cache.version < GM_info.script.version) {
        window.review_cache = {get_reviews, reload, version: GM_info.script.version};
    }

    // Fetch reviews from storage
    function get_reviews() {
        wkof.include('Apiv2');
        return wkof.ready('Apiv2').then(load_data).then(update_data);
    }

    // Deletes cache and refetches reviews
    function reload() {
        return wkof.file_cache.delete('review_cache').then(get_reviews);
    }

    // Loads data from cache
    function load_data() {
        return wkof.file_cache.load('review_cache').then(decompress, _=>{return {cache_version, date: "1970-01-01T00:00:00.000Z", reviews: [],};});
    }

    // Save cache
    function save(data) {
        return wkof.file_cache.save('review_cache', compress(data)).then(_=>data);
    }

    // Compress and decompress the dates for better use of storage space.
    // Dates are stored as time elapesed between items, but are returned as absolute dates
    function compress(data) {return press(true, data);}
    function decompress(data) {return press(false, data);}
    function press(com, data) {
        let last = 0;
        let pressed = data.reviews.map(item => {
                let map = [com ? (item[0]-last) : (last+item[0]), ...item.slice(1)];
                last = com ? item[0] : last+item[0];
                return map;
            });
        return {cache_version: data.cache_version, date: data.date, reviews: pressed};
    }

    // Updates the cache
    async function update_data(data) {
        if (!data.cache_version || data.cache_version < cache_version) data = {cache_version, date: "1970-01-01T00:00:00.000Z", reviews: [],}
        let [date, new_reviews] = await fetch_new_reviews(data.date);
        if (new_reviews.length) {
            for (let new_review of new_reviews) data.reviews.push(new_review);
            data.reviews.sort((a,b) => a[0]<b[0] ? -1 : 1);
            data.date = date;
            save(data);
        }
        return data.reviews;
    }

    // Fetches any new reviews from the API
    async function fetch_new_reviews(last_fetch) {
        let updated_reviews = await wkof.Apiv2.fetch_endpoint('reviews', {filters: {updated_after: last_fetch}});
        let new_reviews = updated_reviews.data.filter(item => last_fetch<item.data.created_at);
        new_reviews = new_reviews.map(item => [
            Date.parse(item.data.created_at),
            item.data.subject_id,
            item.data.starting_srs_stage,
            item.data.incorrect_meaning_answers,
            item.data.incorrect_reading_answers,
        ]);
        return [updated_reviews.data_updated_at, new_reviews];
    }
})(window.wkof);
// ==UserScript==
// @name         Ganbarometer-debug
// @namespace    http://tampermonkey.net/
// @version      0.6a
// @description  Add Difficulty, Load, and Speed gauges to the Wanikani Dashboard
// @author       Rex Walters (Rrwrex AKA rw [at] pobox.com)
// @copyright    2021 Rex Robert Walters
// @license      MIT-0 https://opensource.org/licenses/MIT-0
// @include      /^https://(www|preview).wanikani.com/(dashboard)?$/
// @require      https://greasyfork.org/scripts/410909-wanikani-review-cache/code/Wanikani:%20Review%20Cache.js
// @grant        none
// ==/UserScript==

(function (wkof) {
  "use strict";

  // This script identifiers for caches, etc.
  const script_id = "ganbarometer";
  const script_name = "Ganbarometer";

  // Ensure WKOF is installed
  if (!wkof) {
    let response = confirm(
      `${script_name} requires WaniKani Open Framework.
Click "OK" to be forwarded to installation instructions.`
    );
    if (response) {
      window.location.href =
        "https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549";
      return;
    }
  }

  // Wait until modules are ready then initiate script
  wkof.include("ItemData, Apiv2, Menu, Settings");
  wkof
    .ready("ItemData, Apiv2, Menu, Settings")
    .then(loadSettings)
    .then(updateSettings)
    .then(installMenu)
    .then(loadCSS)
    .then(render);

  // Install our link under [Scripts -> Demo -> Settings Demo]
  function installMenu() {
    wkof.Menu.insert_script_link({
      name: script_name,
      submenu: "Settings",
      title: "GanbarOmeter",
      on_click: openSettings,
    });
  }

  const settings = {};

  let defaults = {
    debug: true, // display debug information
    interval: 72, // Number of hours to summarize reviews over
    sessionIntervalMax: 10, // max minutes between reviews in same session
    normalApprenticeQty: 100, // normal number of items in apprentice queue
    newKanjiWeighting: 0.05, // 0.05 => 10 new kanji make it 50% harder
    normalMisses: 20, // no additional weighting for up to 20% of daily reviews
    extraMissesWeighting: 0.03, // 0.03 => 10 extra misses make it 30% harder
    maxLoad: 300, // maximum number of reviews per day in load graph (50% is normal)
    maxSpeed: 30, // maximum number of seconds per review in speed graph (50% is normal)
    backgroundColor: "#f4f4f4", // section background color
  };

  function loadSettings() {
    return wkof.Settings.load(script_id, defaults);
  }

  function openSettings() {
    let config = {
      script_id: script_id,
      title: script_name,
      on_save: updateSettings,
      content: {
        interval: {
          type: "number",
          label: "Running Average Hours",
          default: defaults.interval,
          hover_tip: "Number of hours to summarize reviews over",
        },
        sessionIntervalMax: {
          type: "number",
          label: "Session interval",
          default: defaults.sessionIntervalMax,
          hover_tip: "Max minutes between reviews in a single session",
        },
        normalApprenticeQty: {
          type: "number",
          label: "Desired apprentice quantity",
          default: defaults.normalApprenticeQty,
          hover_tip: "Number of desired items in the Apprentice bucket",
        },
        newKanjiWeighting: {
          type: "number",
          label: "New kanji weighting factor",
          default: defaults.newKanjiWeighting,
          hover_tip:
            "A value of 0.05 means 10 kanji in stages 1 & 2 imply 50% higher difficulty",
        },
        normalMisses: {
          type: "number",
          label: "Typical percentage of items missed during reviews",
          default: defaults.normalMisses,
          hover_tip:
            "Only misses beyond this percentage are weighted more heavily",
        },
        extraMissesWeighting: {
          type: "number",
          label: "Extra misses weighting",
          default: defaults.extraMissesWeighting,
          hover_tip:
            "A value of 0.03 means 10 extra misses imply 30% higher difficulty",
        },
        maxLoad: {
          type: "number",
          label: "Maximum reviews per day",
          default: defaults.maxLoad,
          hover_tip: "This should be 2X the typical number of reviews/day",
        },
        maxSpeed: {
          type: "number",
          label: "Maximum number of seconds per review",
          default: defaults.maxSpeed,
          hover_tip: "This should be 2x the typical number of seconds/review",
        },
        backgroundColor: {
          type: "color",
          label: "Background color",
          default: defaults.backgroundColor,
          hover_tip: "Background color for theming",
        },
        debug: {
          type: "checkbox",
          label: "Debug",
          default: defaults.debug,
          hover_tip: "Display debug info on console?",
        },
      },
    };
    let dialog = new wkof.Settings(config);
    dialog.open();
  }

  function updateSettings() {
    settings.debug = wkof.settings.ganbarometer.debug;
    settings.interval = wkof.settings.ganbarometer.interval;
    settings.sessionIntervalMax = wkof.settings.ganbarometer.sessionIntervalMax;
    settings.normalApprenticeQty =
      wkof.settings.ganbarometer.normalApprenticeQty;
    settings.newKanjiWeighting = wkof.settings.ganbarometer.newKanjiWeighting;
    settings.normalMisses = wkof.settings.ganbarometer.normalMisses / 100;
    settings.extraMissesWeighting =
      wkof.settings.ganbarometer.extraMissesWeighting;
    settings.maxLoad = wkof.settings.ganbarometer.maxLoad;
    settings.maxSpeed = wkof.settings.ganbarometer.maxSpeed;
    settings.backgroundColor = wkof.settings.ganbarometer.backgroundColor;
    wkof.Settings.save(script_id);
  }

  let css = "";

  function loadCSS() {
    css = `
.${script_id} {
  display:flex;
  justify-content: space-around;
  background-color: ${settings.backgroundColor};
  border-radius: 5px;
  overflow: hidden;
  flex-wrap: wrap;
}

.${script_id} h1 {
  font-size: 18px;
  font-weight: 600;
  margin: 0;
}

.${script_id} p {
  font-size: 10px;
  margin: 0;
}

.${script_id} label {
  margin: 0;
  text-align: center;
  width: 100%;
  padding: 0 10px;
  font-size: 12px;
  color: #bbb;
}

.gauge {
  width: 100%;
  min-width: 120px;
  max-width: 150px;
  padding: 0 10px;
  color: #004033;
  display: flex;
  flex-direction: column;
  align-items: center;
  background-color: ${settings.backgroundColor};
}

.gauge__body {
  width: 100%;
  height: 0;
  padding-bottom: 50%;
  background: #b4c0be;
  position: relative;
  border-top-left-radius: 100% 200%;
  border-top-right-radius: 100% 200%;
  overflow: hidden;
}

.gauge__fill {
  position: absolute;
  top: 100%;
  left: 0;
  width: inherit;
  height: 100%;
  background: #59c273;
  transform-origin: center top;
  transform: rotate(0.25turn);
  transition: transform 0.2s ease-out;
}

.gauge__cover {
  width: 75%;
  height: 150%;
  background-color: ${settings.backgroundColor};
  border-radius: 50%;
  position: absolute;
  top: 25%;
  left: 50%;
  transform: translateX(-50%);

  /* Text */
  display: flex;
  align-items: center;
  justify-content: center;
  padding-bottom: 25%;
  box-sizing: border-box;
  font-size: 25px;
}
    `;
  }

  // The metrics we want to retrieve and display
  const metrics = {
    reviewed: 0, // total number of items reviewed over interval
    sessions: [], // array of Session objects
    apprentice: 0, // total number of items currently in Apprentice (stages 1-4)
    newKanji: 0, // total number of radicals & kanji in stages 1 or 2
    minutes: function () {
      // total number of minutes spent reviewing over interval
      let min = 0;
      for (let sess of this.sessions) {
        min += sess.minutes();
      }
      return min;
    },
    missesPerDay: function () {
      // number of review items answered incorrectly over interval
      let s = 0;
      for (let sess of this.sessions) {
        s += sess.misses;
      }
      s = (s * 24) / settings.interval;
      return s;
    },
    reviewsPerDay: function () {
      // reviews-per-day averaged over the interval
      return Math.round((this.reviewed * 24) / settings.interval);
    },
    secondsPerReview: function () {
      // seconds-per-review averaged over the sessions
      return Math.round((60 * this.minutes()) / this.reviewed);
    },
    difficulty: function () {
      // return a value from 0 to 1, with 0.5 representing "normal"
      // Normal = ~100 items in Apprentice bucket (stages 1-4)
      let raw = this.apprentice / (2 * settings.normalApprenticeQty);

      // Heuristic 1: new kanji are harder than other apprentice items
      // raw +=
      //   (this.newKanji * settings.newKanjiWeighting) /
      //   (2 * settings.normalApprenticeQty);
      raw = raw * (1 + this.newKanji * settings.newKanjiWeighting);

      // Heuristic 2: missed items are harder than other apprentice items
      let allowedMisses = Math.round(
        settings.normalMisses * this.reviewsPerDay
      );
      let extraMisses = this.missesPerDay - allowedMisses;
      if (extraMisses > 0) {
        raw = raw * (1 + extraMisses * settings.extraMissesWeighting);
      }

      return raw > 1 ? 1 : raw;
    },
    load: function () {
      // returns a value betweeen 0 and 1 representing the percentage of reviews
      // per day relative to maxLoad
      let raw = this.reviewsPerDay() / settings.maxLoad;
      return raw > 1 ? 1 : raw;
    },
    speed: function () {
      // returns a value between 0 and 1 representing the percentage of seconds
      // per review relative to maxSpeed
      let raw = this.secondsPerReview() / settings.maxSpeed;
      return raw > 1 ? 1 : raw;
    },
  };

  /*
   * ********* MAIN function to calculate and display metrics ********
   */
  async function render(itemData, apiv2) {
    // Get all reviews within interval hours of now
    let firstReviewDate = new Date(
      Date.now() - settings.interval * 60 * 60 * 1000
    );
    let options = {
      last_update: firstReviewDate,
    };
    console.log(`Ganbarometer fetch options:`);
    console.log(Date());
    console.log(options);
    debugger;

    let reviewCollection = await wkof.Apiv2.fetch_endpoint("reviews", options);
    let newReviews = reviewCollection.data;

    // Save our first metric
    metrics.reviewed = newReviews.length;

    // Calculate and save our second set of metrics
    // findSessions() returns an Array of Session objects
    metrics.sessions = findSessions(newReviews);

    // Finally, retrieve and save the apprentice and newKanji metrics
    let config = {
      wk_items: {
        filters: {
          srs: "appr1, appr2, appr3, appr4",
        },
      },
    };
    let items = await wkof.ItemData.get_items(config);
    metrics.apprentice = items.length;
    config = {
      wk_items: {
        filters: {
          srs: "appr1, appr2",
          item_type: "kan",
        },
      },
    };
    items = await wkof.ItemData.get_items(config);
    metrics.newKanji = items.length;

    // Optionally log what we've extracted
    if (settings.debug) {
      logMetrics(metrics);
    }

    // Now populate the section and add it to the dashboard
    updateDashboard(metrics, settings);
  }

  function logMetrics(metrics) {
    console.log(
      `------ GanbarOmeter debug output ------
settings:
  - interval: ${settings.interval}
  - sessionIntervalMax: ${settings.sessionIntervalMax}
  - normalApprenticeQty: ${settings.normalApprenticeQty}
  - newKanjiWeighting: ${settings.newKanjiWeighting}
  - normalMisses: ${settings.normalMisses}
  - extraMissesWeighting: ${settings.extraMissesWeighting}
  - maxLoad: ${settings.maxLoad}
  - maxSpeed: ${settings.maxSpeed}
  - backgroundColor: ${settings.backgroundColor}
${metrics.reviewed} reviews in ${settings.interval} hours
${Math.round(10 * metrics.missesPerDay()) / 10} misses per day
${metrics.minutes()} total minutes
${metrics.sessions.length} sessions:`
    );
    metrics.sessions.forEach((s) => {
      console.log(
        `     - Start: ${s.startTime}
       End: ${s.endTime}
       Misses: ${s.misses}
       Reviews: ${s.len}
       Review minutes: ${s.minutes()}`
      );
    });
    console.log(
      `${metrics.apprentice} apprentice ${metrics.newKanji} newKanji`
    );
    console.log(
      `${metrics.reviewsPerDay()} reviews per day (0 - ${settings.maxLoad}`
    );
    console.log(
      `${metrics.secondsPerReview()} seconds per review (0 - ${
        settings.maxSpeed
      })`
    );
    console.log(`Difficulty: ${metrics.difficulty()} (0-1)`);
    console.log(`Load: ${metrics.load()}`);
    console.log(`Speed: ${metrics.speed()}`);
    console.log(`------ End GanbarOmeter ------`);
  }

  // Create an html <section> for our metrics and add to dashboard
  function updateDashboard(metrics, settings) {
    // Append our styling to the head of the doucment
    const gbStyle = document.createElement("style");
    gbStyle.id = script_id + "CSS";
    gbStyle.innerHTML = css;
    document.querySelector("head").append(gbStyle);

    let html =
      `<label>Daily averages for the past ${settings.interval} hours</label>` +
      renderDiv(
        "gbDifficulty",
        "Difficulty",
        `${metrics.apprentice} (${metrics.newKanji}k/${Math.round(
          metrics.missesPerDay()
        )}m)`
      ) +
      renderDiv("gbLoad", "Load", "reviews/day") +
      renderDiv("gbSpeed", "Speed", "sec/review");

    // Create a section for our content
    const gbSection = document.createElement("Section");
    gbSection.classList.add(`${script_id}`);
    gbSection.innerHTML = html;

    let gauge = gbSection.querySelector("#gbDifficulty");
    setGaugeValue(gauge, metrics.difficulty());

    gauge = gbSection.querySelector("#gbLoad");
    setGaugeValue(gauge, metrics.load(), `${metrics.reviewsPerDay()}`);

    gauge = gbSection.querySelector("#gbSpeed");
    setGaugeValue(gauge, metrics.speed(), `${metrics.secondsPerReview()}`);

    // Now add our new section at the just before the forum list
    document.querySelector(".progress-and-forecast").before(gbSection);
  }

  function renderDiv(id, title, text) {
    return `<div id="${id}" class="gauge">
    <h1>${title}</h1>
    <div class="gauge__body">
      <div class="gauge__fill"></div>
      <div class="gauge__cover"></div>
    </div>
    <p>${text}</p>
  </div>`;
  }

  function setGaugeValue(gauge, value, displayValue) {
    if (value < 0 || value > 1) {
      return;
    }

    let display = displayValue ? displayValue : `${Math.round(value * 100)}%`;

    gauge.querySelector(".gauge__fill").style.transform = `rotate(${
      value / 2
    }turn)`;
    gauge.querySelector(".gauge__cover").textContent = display;

    if (value >= 0.9) {
      gauge.querySelector(".gauge__fill").style.backgroundColor = "#e50036";
    } else if (value >= 0.8) {
      gauge.querySelector(".gauge__fill").style.backgroundColor = "#ece619";
    }
  }

  // Function to return a filtered array of reviews
  // older than the specified number of hours
  function filterRecent(reviews, hours) {
    return reviews.filter(
      // a[0] = creationDate
      (a) => a[0] > Date.now() - hours * 60 * 60 * 1000
    );
  }

  // A Session object holds an index into an array of reviews, plus a length
  // Define a Session object
  function Session(firstIndex, length, startTime, endTime, misses) {
    this.firstIndex = firstIndex; // index of first review in this session
    this.len = length; // number of reviews in this session
    this.startTime = startTime; // start time of first review (Date object)
    this.endTime = endTime; // start(!!) time of final review (Date object)
    this.misses = misses; // "miss" means one or more incorrect answers (reading or meaning)
    this.minutes = function () {
      // number of minutes spent reviewing in this session
      let raw =
        endTime - startTime < settings.maxSpeed * 100
          ? Math.round((this.endTime - this.startTime) / (1000 * 60))
          : settings.maxSpeed / 2; // assume single review session speed is typical
      return raw;
    };
  }

  function findSessions(reviews) {
    // Start with an empty array of sessions
    let sessions = [];
    // Get the time of the first review
    let firstTime =
      reviews.length > 0 ? new Date(reviews[0].data_updated_at) : new Date(0);

    // Initialize what will become sessions[0]
    let curSession = new Session(
      0, // firstIndex - start with reviews[0]
      0, // length (currently unknown, initialize to zero)
      firstTime, // startTime is time of first review
      firstTime, // endTime (currently unknown, initialize to startTime)
      0 // misses (currently unknown, initialize to zero)
    );

    // Now iterate through reviews to find sessions
    // note that reviews[0] is guaranteed to be within the current session!
    reviews.forEach((review) => {
      if (
        withinSession(
          curSession.endTime, // prevTime
          new Date(review.data_updated_at), // newTime
          settings.sessionIntervalMax // maxMinutes
        )
      ) {
        // Still within a session, so increment the length
        curSession.len += 1;
        // "miss" means one or more incorrect meaning or reading answers
        curSession.misses +=
          review.data.incorrect_meaning_answers +
            review.data.incorrect_reading_answers >
          0
            ? 1
            : 0;
        // Update endTime the the time of this review
        curSession.endTime = new Date(review.data_updated_at);
      } else {
        // Finished prior session and starting a new one
        sessions.push(curSession);
        // And create a new curSession of length 1 for this review
        let newIndex = curSession.firstIndex + curSession.len;
        let newDate = new Date(review.data_updated_at);
        let curMisses =
          review.incorrect_meaning_answers +
            review.data.incorrect_reading_answers >
          0
            ? 1
            : 0;
        curSession = new Session(newIndex, 1, newDate, newDate, curMisses);
      }
    });
    // Don't forget the last session when we fall out of the loop
    sessions.push(curSession);
    return sessions;
  }

  // Determine if newTime is within maxMinutes of prevTime
  function withinSession(prevTime, newTime, maxMinutes) {
    let timeDifference = newTime - prevTime;
    return timeDifference <= maxMinutes * 1000 * 60 * 60;
  }
})(window.wkof);

})}})(that.context, that.fapply, that.console);
//# sourceURL=moz-extension://5612df33-f66b-cc47-bab7-950e074e1939/userscripts/Ganbarometer-debug.user.js?id=4a8a2863-bb88-4be6-a27a-fdbd0430a897
})((()=>{const k="__u__14844144.954712437",r=this[k];delete this[k];return r;})())

That’s the actual script.
:grin:

Please click the console and take a screen shot (the lines with a white background starting “------ GanbarOmeter debug output ------”)

It should look something like this:

------ GanbarOmeter debug output ------
settings:
  - interval: 72
  - sessionIntervalMax: 10
  - normalApprenticeQty: 100
  - newKanjiWeighting: 0.05
  - normalMisses: 0.002
  - extraMissesWeighting: 0.03
  - maxLoad: 300
  - maxSpeed: 30
  - backgroundColor: #f4f4f4
372 reviews in 72 hours
25.7 misses per day
106 total minutes
3 sessions:
userscript.html?name=Ganbarometer.user.js&id=83c27782-084f-4b15-8ae8-536560660314:470      - Start: Fri Sep 17 2021 09:07:00 GMT-0700 (Pacific Daylight Time)
       End: Fri Sep 17 2021 09:35:46 GMT-0700 (Pacific Daylight Time)
       Misses: 22
       Reviews: 125
       Review minutes: 29
userscript.html?name=Ganbarometer.user.js&id=83c27782-084f-4b15-8ae8-536560660314:470      - Start: Sat Sep 18 2021 10:47:27 GMT-0700 (Pacific Daylight Time)
       End: Sat Sep 18 2021 11:38:31 GMT-0700 (Pacific Daylight Time)
       Misses: 30
       Reviews: 139
       Review minutes: 51
userscript.html?name=Ganbarometer.user.js&id=83c27782-084f-4b15-8ae8-536560660314:470      - Start: Sun Sep 19 2021 09:02:29 GMT-0700 (Pacific Daylight Time)
       End: Sun Sep 19 2021 09:28:49 GMT-0700 (Pacific Daylight Time)
       Misses: 25
       Reviews: 108
       Review minutes: 26
userscript.html?name=Ganbarometer.user.js&id=83c27782-084f-4b15-8ae8-536560660314:478 97 apprentice 4 newKanji
userscript.html?name=Ganbarometer.user.js&id=83c27782-084f-4b15-8ae8-536560660314:481 124 reviews per day (0 - 300
userscript.html?name=Ganbarometer.user.js&id=83c27782-084f-4b15-8ae8-536560660314:484 17 seconds per review (0 - 30)
userscript.html?name=Ganbarometer.user.js&id=83c27782-084f-4b15-8ae8-536560660314:489 Difficulty: 0.582 (0-1)
userscript.html?name=Ganbarometer.user.js&id=83c27782-084f-4b15-8ae8-536560660314:490 Load: 0.41333333333333333
userscript.html?name=Ganbarometer.user.js&id=83c27782-084f-4b15-8ae8-536560660314:491 Speed: 0.5666666666666667
userscript.html?name=Ganbarometer.user.js&id=83c27782-084f-4b15-8ae8-536560660314:492 ------ End GanbarOmeter ------

Also: I think (but am not completely sure) that WK caches stats on the server side. So the first time the script requests three days of reviews will take much longer than subsequent requests (until the next time they clear the cache on the server). They may only use the cache for a few hours or a day, meaning the first time using the script each day might be slow.

ah, I feel rather silly now :smiley:

------ GanbarOmeter debug output ------
settings:
  - interval: 72
  - sessionIntervalMax: 10
  - normalApprenticeQty: 100
  - newKanjiWeighting: 0.05
  - normalMisses: 0.2
  - extraMissesWeighting: 0.03
  - maxLoad: 300
  - maxSpeed: 30
  - backgroundColor: #f4f4f4
1331 reviews in 72 hours
104.7 misses per day
1497 total minutes
3 sessions: Ganbarometer-debug.user.js:456:13
     - Start: Fri Sep 17 2021 12:05:21 GMT+0700 (Indochina Time)
       End: Fri Sep 17 2021 16:52:35 GMT+0700 (Indochina Time)
       Misses: 110
       Reviews: 441
       Review minutes: 287 Ganbarometer-debug.user.js:474:15
     - Start: Sat Sep 18 2021 11:41:19 GMT+0700 (Indochina Time)
       End: Sat Sep 18 2021 22:01:29 GMT+0700 (Indochina Time)
       Misses: 92
       Reviews: 391
       Review minutes: 620 Ganbarometer-debug.user.js:474:15
     - Start: Sun Sep 19 2021 09:32:33 GMT+0700 (Indochina Time)
       End: Sun Sep 19 2021 19:22:51 GMT+0700 (Indochina Time)
       Misses: 112
       Reviews: 499
       Review minutes: 590 Ganbarometer-debug.user.js:474:15
296 apprentice 2 newKanji Ganbarometer-debug.user.js:482:13
444 reviews per day (0 - 300 Ganbarometer-debug.user.js:485:13
67 seconds per review (0 - 30) Ganbarometer-debug.user.js:488:13
Difficulty: 1 (0-1) Ganbarometer-debug.user.js:493:13
Load: 1 Ganbarometer-debug.user.js:494:13
Speed: 1 Ganbarometer-debug.user.js:495:13
------ End GanbarOmeter ------ Ganbarometer-debug.user.js:496:13

​


1 Like

No reason to!

Now please change the settings for the script by clicking your profile icon on the upper right of the screen and navigating to Settings → Ganbarometer.

Change Session interval to something really short. Let’s try 1 minute first.

Then refresh the page in your browser and post the new console output. I expect to see more sessions and a more reasonable speed.

Since you’re still running the debug version, you’ll need to hit the play button again when it hits the debugger statement.

20s/r! that’s more like it!

------ GanbarOmeter debug output ------
settings:
  - interval: 72
  - sessionIntervalMax: 1
  - normalApprenticeQty: 100
  - newKanjiWeighting: 0.05
  - normalMisses: 0.2
  - extraMissesWeighting: 0.03
  - maxLoad: 300
  - maxSpeed: 30
  - backgroundColor: #f4f4f4
1331 reviews in 72 hours
104.7 misses per day
439 total minutes
8 sessions: Ganbarometer-debug.user.js:456:13
     - Start: Fri Sep 17 2021 12:05:21 GMT+0700 (Indochina Time)
       End: Fri Sep 17 2021 12:08:23 GMT+0700 (Indochina Time)
       Misses: 5
       Reviews: 19
       Review minutes: 3 Ganbarometer-debug.user.js:474:15
     - Start: Fri Sep 17 2021 14:18:36 GMT+0700 (Indochina Time)
       End: Fri Sep 17 2021 16:52:35 GMT+0700 (Indochina Time)
       Misses: 105
       Reviews: 422
       Review minutes: 154 Ganbarometer-debug.user.js:474:15
     - Start: Sat Sep 18 2021 11:41:19 GMT+0700 (Indochina Time)
       End: Sat Sep 18 2021 12:10:27 GMT+0700 (Indochina Time)
       Misses: 44
       Reviews: 140
       Review minutes: 29 Ganbarometer-debug.user.js:474:15
     - Start: Sat Sep 18 2021 14:36:02 GMT+0700 (Indochina Time)
       End: Sat Sep 18 2021 15:15:14 GMT+0700 (Indochina Time)
       Misses: 21
       Reviews: 144
       Review minutes: 39 Ganbarometer-debug.user.js:474:15
     - Start: Sat Sep 18 2021 16:33:25 GMT+0700 (Indochina Time)
       End: Sat Sep 18 2021 16:41:17 GMT+0700 (Indochina Time)
       Misses: 10
       Reviews: 46
       Review minutes: 8 Ganbarometer-debug.user.js:474:15
     - Start: Sat Sep 18 2021 21:54:33 GMT+0700 (Indochina Time)
       End: Sat Sep 18 2021 22:01:29 GMT+0700 (Indochina Time)
       Misses: 17
       Reviews: 61
       Review minutes: 7 Ganbarometer-debug.user.js:474:15
     - Start: Sun Sep 19 2021 09:32:33 GMT+0700 (Indochina Time)
       End: Sun Sep 19 2021 09:48:13 GMT+0700 (Indochina Time)
       Misses: 10
       Reviews: 57
       Review minutes: 16 Ganbarometer-debug.user.js:474:15
     - Start: Sun Sep 19 2021 16:20:01 GMT+0700 (Indochina Time)
       End: Sun Sep 19 2021 19:22:51 GMT+0700 (Indochina Time)
       Misses: 102
       Reviews: 442
       Review minutes: 183 Ganbarometer-debug.user.js:474:15
296 apprentice 2 newKanji Ganbarometer-debug.user.js:482:13
444 reviews per day (0 - 300 Ganbarometer-debug.user.js:485:13
20 seconds per review (0 - 30) Ganbarometer-debug.user.js:488:13
Difficulty: 1 (0-1) Ganbarometer-debug.user.js:493:13
Load: 1 Ganbarometer-debug.user.js:494:13
Speed: 0.6666666666666666 Ganbarometer-debug.user.js:495:13
------ End GanbarOmeter ------ Ganbarometer-debug.user.js:496:13

​

edit: although according to the heatmap it should be 12? heh, I’m not sure what’s the correct number anymore :thinking: it says 439 total minutes, but heatmap gives something like 270 (should be less since it’s a bit more than 72h)

Yup. It went from finding 3 “review sessions” to 8.

Summing the review times of the 8 sessions is much shorter than summing across the three longer ones, because it doesn’t include the inactive time between the sessions.

I’ll lower the default session interval in the next version of the script.

Please DELETE the ganbarometer-debug script in your tampermonkey dashboard. Then re-enable the real one.

Please let me know if you still experience very long load times. As I said, I think this may still be unavoidable on occasion (still unsure).

Thanks for all your help with debugging!

1 Like

It’s more than likely that @Kumirei and I are calculating things differently. I’ll try to take some time to compare her code to mine, but it may take a few days.

I’m pretty confident that the numbers I’m displaying are calculated correctly, however.

It took 439 minutes for you to review 1331 items over the past three days. (439 * 60)/1331 = 19.789 seconds (I round up).

IMPORTANT EDIT

I forgot to mention that an early version of this script used the same review_cache that Heatmap uses. I quite intentionally changed it to call the API directly (well from WKOF) rather than using the review_cache, expressly to speed up performance for users that WEREN’T using HEATMAP.

By default, the Heatmap loads all reviews since you joined. This can be hundreds of thousands of records which can take a very long time. That’s why the review_cache was born (to cache results on each user’s local computer).

Since I only need a few days worth of records, I figured it was better to eliminate a dependency and just call the API directly.

In my case, I use both and it can take quite a while for Heatmap to load. I leave it disabled most of the time because of it. (It’s possible to also reconfigure Heatmap’s settings to only display more recent data and not retrieve everything, but I haven’t done so.)

Anyway, all of this is to say that there may be some weird interaction between these two scripts (I worry that my calls to the API may somehow invalidate the cache). Further, I may choose in a future version of Ganbarometer to use the review_cache if Heatmap is loaded, otherwise call the AP directly.

2 Likes

actually, I’m not sure if this is relevant, but I reduced the session interval parameter way down to 0.1 and now Ganbarometer’s measurement agrees completely with Heatmap (12s/r exactly). I suspect this is because I have a tendency to quickly go in and out of review sessions.

Anyhow, thanks very much for your help! :smiley:

VERY interesting!

How many sessions did it find with it at 0.1?

[Edit]

I need to think about a reasonable default with a fresh brain in the morning.

In my case I average around 15s/review on average for real, but I take MUCH longer for some items than others. Many I know instantly and it’s basically as fast as I can type. Other times within the same session I’m pondering much longer before answering (often incorrectly).

I think the next version will include some additional telemetry to report the breakdown of measured intervals between reviews. I suspect there will usually be effectively three buckets in the Pareto. Something like:

  • 5-10 seconds (know instantly)
  • A minute or two (pondering)
  • 10 minutes (breaks between sessions)

17, it would seem.

------ GanbarOmeter debug output ------
settings:
  - interval: 72
  - sessionIntervalMax: 0.1
  - normalApprenticeQty: 100
  - newKanjiWeighting: 0.05
  - normalMisses: 0.2
  - extraMissesWeighting: 0.03
  - maxLoad: 300
  - maxSpeed: 30
  - backgroundColor: #f4f4f4
1331 reviews in 72 hours
104 misses per day
256 total minutes
17 sessions: Ganbarometer.user.js:452:13
     - Start: Fri Sep 17 2021 12:05:21 GMT+0700 (Indochina Time)
       End: Fri Sep 17 2021 12:08:23 GMT+0700 (Indochina Time)
       Misses: 5
       Reviews: 19
       Review minutes: 3 Ganbarometer.user.js:470:15
     - Start: Fri Sep 17 2021 14:18:36 GMT+0700 (Indochina Time)
       End: Fri Sep 17 2021 14:43:29 GMT+0700 (Indochina Time)
       Misses: 23
       Reviews: 79
       Review minutes: 25 Ganbarometer.user.js:470:15
     - Start: Fri Sep 17 2021 14:50:03 GMT+0700 (Indochina Time)
       End: Fri Sep 17 2021 14:55:17 GMT+0700 (Indochina Time)
       Misses: 7
       Reviews: 21
       Review minutes: 5 Ganbarometer.user.js:470:15
     - Start: Fri Sep 17 2021 15:16:14 GMT+0700 (Indochina Time)
       End: Fri Sep 17 2021 16:07:45 GMT+0700 (Indochina Time)
       Misses: 56
       Reviews: 213
       Review minutes: 52 Ganbarometer.user.js:470:15
     - Start: Fri Sep 17 2021 16:38:47 GMT+0700 (Indochina Time)
       End: Fri Sep 17 2021 16:52:35 GMT+0700 (Indochina Time)
       Misses: 19
       Reviews: 109
       Review minutes: 14 Ganbarometer.user.js:470:15
     - Start: Sat Sep 18 2021 11:41:19 GMT+0700 (Indochina Time)
       End: Sat Sep 18 2021 12:10:27 GMT+0700 (Indochina Time)
       Misses: 44
       Reviews: 140
       Review minutes: 29 Ganbarometer.user.js:470:15
     - Start: Sat Sep 18 2021 14:36:02 GMT+0700 (Indochina Time)
       End: Sat Sep 18 2021 14:48:47 GMT+0700 (Indochina Time)
       Misses: 19
       Reviews: 90
       Review minutes: 13 Ganbarometer.user.js:470:15
     - Start: Sat Sep 18 2021 15:08:12 GMT+0700 (Indochina Time)
       End: Sat Sep 18 2021 15:15:14 GMT+0700 (Indochina Time)
       Misses: 2
       Reviews: 54
       Review minutes: 7 Ganbarometer.user.js:470:15
     - Start: Sat Sep 18 2021 16:33:25 GMT+0700 (Indochina Time)
       End: Sat Sep 18 2021 16:41:17 GMT+0700 (Indochina Time)
       Misses: 10
       Reviews: 46
       Review minutes: 8 Ganbarometer.user.js:470:15
     - Start: Sat Sep 18 2021 21:54:33 GMT+0700 (Indochina Time)
       End: Sat Sep 18 2021 22:01:29 GMT+0700 (Indochina Time)
       Misses: 17
       Reviews: 61
       Review minutes: 7 Ganbarometer.user.js:470:15
     - Start: Sun Sep 19 2021 09:32:33 GMT+0700 (Indochina Time)
       End: Sun Sep 19 2021 09:48:13 GMT+0700 (Indochina Time)
       Misses: 10
       Reviews: 57
       Review minutes: 16 Ganbarometer.user.js:470:15
     - Start: Sun Sep 19 2021 16:20:01 GMT+0700 (Indochina Time)
       End: Sun Sep 19 2021 16:45:56 GMT+0700 (Indochina Time)
       Misses: 28
       Reviews: 141
       Review minutes: 26 Ganbarometer.user.js:470:15
     - Start: Sun Sep 19 2021 17:13:44 GMT+0700 (Indochina Time)
       End: Sun Sep 19 2021 17:13:54 GMT+0700 (Indochina Time)
       Misses: 0
       Reviews: 2
       Review minutes: 0 Ganbarometer.user.js:470:15
     - Start: Sun Sep 19 2021 17:32:52 GMT+0700 (Indochina Time)
       End: Sun Sep 19 2021 17:37:19 GMT+0700 (Indochina Time)
       Misses: 4
       Reviews: 15
       Review minutes: 4 Ganbarometer.user.js:470:15
     - Start: Sun Sep 19 2021 18:07:59 GMT+0700 (Indochina Time)
       End: Sun Sep 19 2021 18:13:02 GMT+0700 (Indochina Time)
       Misses: 13
       Reviews: 41
       Review minutes: 5 Ganbarometer.user.js:470:15
     - Start: Sun Sep 19 2021 18:21:52 GMT+0700 (Indochina Time)
       End: Sun Sep 19 2021 18:42:43 GMT+0700 (Indochina Time)
       Misses: 20
       Reviews: 121
       Review minutes: 21 Ganbarometer.user.js:470:15
     - Start: Sun Sep 19 2021 19:01:48 GMT+0700 (Indochina Time)
       End: Sun Sep 19 2021 19:22:51 GMT+0700 (Indochina Time)
       Misses: 35
       Reviews: 122
       Review minutes: 21 Ganbarometer.user.js:470:15
296 apprentice 2 newKanji Ganbarometer.user.js:478:13
444 reviews per day (0 - 300 Ganbarometer.user.js:481:13
12 seconds per review (0 - 30) Ganbarometer.user.js:484:13
Difficulty: 1 (0-1) Ganbarometer.user.js:489:13
Load: 1 Ganbarometer.user.js:490:13
Speed: 0.4 Ganbarometer.user.js:491:13
------ End GanbarOmeter ------ Ganbarometer.user.js:492:13

​


some context: I actually sometimes leave the review sessions open until they timeout (when I’m finding some backing songs on YouTube or something). When I get back I immediately reload; I’m not sure how this might contribute to the count but I think it could play a role (then again, not a programmer here!)

Note that the interval between the second and third interval is only 6.5 minutes.

Do you think this was a real break between study sessions, or should it be considered “pondering” time within the same session?

EDIT: Something is weird between your 1minute results and your 0.1 minute results. I’m going to call it a night, but I think you’ve found a bug.

1 Like