内存监控控制台(全背景图表)

2025 年 1 月 17 日

// ==UserScript==
// @name        内存监控控制台(全背景图表)
// @namespace   Violentmonkey Scripts
// @match       *://*/*
// @grant       none
// @version     2.6
// @author      K
// @description 2025 年 1 月 17 日
// @license MIT
// ==/UserScript==

(function() {
  class MemoryStats {
    constructor() {
      this.container = document.createElement('div');
      this.container.id = 'memoryStatsContainer';

      this.canvas = document.createElement('canvas');
      this.container.appendChild(this.canvas);

      this.ctx = this.canvas.getContext('2d');
      this.ctx.strokeStyle = 'rgba(0, 255, 0, 0.8)';
      const gradient = this.ctx.createLinearGradient(0, 0, 0, this.canvas.height);
      gradient.addColorStop(0, 'rgba(0, 255, 0, 0.2)');
      gradient.addColorStop(1, 'rgba(0, 255, 0, 0.05)');
      this.ctx.fillStyle = gradient;

      this.MAX_DATA_POINTS = 200;
      this.values = new Array(this.MAX_DATA_POINTS).fill(0);
      this.maxMem = 0;

      window.addEventListener('resize', () => this.resize());
    }

    setMaxMem(value) {
      this.maxMem = value;
    }

    update(value) {
      const WIDTH = this.canvas.width;
      const HEIGHT = this.canvas.height;
      const CHART_HEIGHT = HEIGHT * 0.7;

      const gradient = this.ctx.createLinearGradient(0, 0, 0, HEIGHT);
      gradient.addColorStop(0, 'rgba(0, 255, 0, 0.2)');
      gradient.addColorStop(1, 'rgba(0, 255, 0, 0.05)');
      this.ctx.fillStyle = gradient;

      for (let i = 0; i < this.MAX_DATA_POINTS - 1; i++) {
        this.values[i] = this.values[i + 1];
      }
      this.values[this.MAX_DATA_POINTS - 1] = value;

      this.ctx.clearRect(0, 0, WIDTH, HEIGHT);
      this.ctx.beginPath();
      this.ctx.moveTo(0, HEIGHT);

      for (let i = 0; i < this.MAX_DATA_POINTS; i++) {
        const x = (i / (this.MAX_DATA_POINTS - 1)) * WIDTH;
        const y = HEIGHT - (this.values[i] / this.maxMem) * CHART_HEIGHT;
        this.ctx.lineTo(x, y);
      }

      this.ctx.lineTo(WIDTH, HEIGHT);
      this.ctx.globalAlpha = 0.5;
      this.ctx.fill();
      this.ctx.globalAlpha = 1;
      this.ctx.stroke();
    }

    resize() {
      this.canvas.width = this.container.clientWidth;
      this.canvas.height = this.container.clientHeight;
      this.update(this.values[this.values.length - 1]);
    }

    get domElement() {
      return this.container;
    }
  }

  class MemoryMonitor {
    constructor() {
      this.timer = null;
      this.maxMem = 0;
      this.memoryStats = new MemoryStats();
      this.memoryInfoDiv = this.addDom();
      this.isDragging = false;
      this.offsetX = 0;
      this.offsetY = 0;

      this.initDragEvents();
      this.initKeyboardEvents();
      this.loadSavedState();
    }

    addDom() {
      const css = `
        .memory-info-div {
          position: fixed;
          bottom: 10px;
          right: 10px;
          border: 1px solid #00FF00;
          background-color: rgba(0, 0, 0, 0.8);
          color: #00FF00;
          padding: 5px;
          z-index: 100000000;
          font-family: 'Courier New', monospace;
          user-select: none;
          display: none;
          cursor: grab;
          width: 200px;
          height: 60px;
          text-align: center;
          border-radius: 5px;
          box-shadow: 0 0 10px rgba(0, 255, 0, 0.3);
          overflow: hidden;
        }
        .memory-info-container {
          position: relative;
          z-index: 2;
          display: flex;
          flex-direction: column;
          justify-content: flex-end;
          height: 100%;
        }
        .memory-info-row {
          display: flex;
          justify-content: space-between;
          align-items: center;
          padding: 0 5px;
        }
        .memory-info-title {
          font-size: 10px;
          line-height: 14px;
          opacity: 0.8;
          color: rgba(0, 255, 0, 0.8);
        }
        .memory-info-value {
          font-size: 14px;
          line-height: 18px;
          font-weight: bold;
          color: #00FF00;
          text-shadow: 0 0 5px rgba(0, 255, 0, 0.5);
        }
        #memoryStatsContainer {
          position: absolute;
          top: 0;
          left: 0;
          right: 0;
          bottom: 0;
          z-index: 1;
          opacity: 1;
        }
        #memoryStatsContainer canvas {
          width: 100%;
          height: 100%;
        }
      `;

      const styleSheet = document.createElement("style");
      styleSheet.type = "text/css";
      styleSheet.innerText = css;
      document.head.appendChild(styleSheet);

      const div = document.createElement('div');
      div.classList.add('memory-info-div');
      div.setAttribute('owl-ignore', '');
      document.body.appendChild(div);
      return div;
    }

    initDragEvents() {
      this.memoryInfoDiv.addEventListener('mousedown', (e) => {
        this.isDragging = true;
        this.offsetX = e.clientX - this.memoryInfoDiv.getBoundingClientRect().left;
        this.offsetY = e.clientY - this.memoryInfoDiv.getBoundingClientRect().top;
        this.memoryInfoDiv.style.cursor = 'grabbing';
      });

      document.addEventListener('mouseup', () => {
        this.isDragging = false;
        this.memoryInfoDiv.style.cursor = 'grab';
      });

      document.addEventListener('mousemove', (e) => {
        if (this.isDragging) {
          this.memoryInfoDiv.style.left = (e.clientX - this.offsetX) + 'px';
          this.memoryInfoDiv.style.top = (e.clientY - this.offsetY) + 'px';
          this.memoryInfoDiv.style.right = 'auto';
          this.memoryInfoDiv.style.bottom = 'auto';

          localStorage.setItem('memoryInfoDivPosition', JSON.stringify({
            left: this.memoryInfoDiv.style.left,
            top: this.memoryInfoDiv.style.top
          }));
        }
      });
    }

    toGB(bytes) {
      const gb = bytes / 1024 / 1024 / 1024;
      const mb = bytes / 1024 / 1024;

      if (gb < 1) {
        return mb.toFixed(0) + 'M';
      } else {
        return gb.toFixed(2) % 1 === 0 ? gb.toFixed(0) + 'G' : gb.toFixed(2) + 'G';
      }
    }

    updateMemoryInfo() {
      const memory = window.performance.memory;
      if (!memory) {
        this.memoryInfoDiv.textContent = "Performance API is not supported in this browser.";
        return;
      }

      this.maxMem = Math.max(memory.usedJSHeapSize, this.maxMem);
      const dynamicMaxMem = Math.min(
        memory.jsHeapSizeLimit,
        Math.max(memory.usedJSHeapSize * 1.5, 512 * 1024 * 1024) // 最小显示 512MB
      );
      this.memoryStats.setMaxMem(dynamicMaxMem);
      const totalMemory = this.toGB(memory.jsHeapSizeLimit);
      const usedMemory = this.toGB(memory.usedJSHeapSize);
      const maxMemory = this.toGB(this.maxMem);
      const usageRate = ((memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100).toFixed(2) + '%';

      this.memoryInfoDiv.innerHTML = `
        <div id="memoryStatsContainer"></div>
        <div class="memory-info-container">
          <div class="memory-info-row">
            <span class="memory-info-title">Max/Total:</span>
            <span class="memory-info-value">${maxMemory}/${totalMemory}</span>
          </div>
          <div class="memory-info-row">
            <span class="memory-info-title">Used:</span>
            <span class="memory-info-value">${usedMemory}</span>
          </div>
          <div class="memory-info-row">
            <span class="memory-info-title">Usage:</span>
            <span class="memory-info-value">${usageRate}</span>
          </div>
        </div>
      `;

      this.memoryInfoDiv.appendChild(this.memoryStats.domElement);
      this.memoryStats.resize();
      this.memoryStats.update(memory.usedJSHeapSize);
    }

    init() {
      clearInterval(this.timer);
      this.timer = setInterval(() => this.updateMemoryInfo(), 150);
      this.memoryInfoDiv.style.display = 'block';
      localStorage.setItem('memoryInfoDivVisible', 'true');
    }

    destroy() {
      clearInterval(this.timer);
      this.memoryInfoDiv.style.display = 'none';
      localStorage.setItem('memoryInfoDivVisible', 'false');
    }

    loadSavedState() {
      const savedVisible = localStorage.getItem('memoryInfoDivVisible');
      const savedPosition = JSON.parse(localStorage.getItem('memoryInfoDivPosition'));

      if (savedVisible === 'true') {
        this.init();
      } else {
        this.destroy();
      }

      if (savedPosition) {
        this.memoryInfoDiv.style.left = savedPosition.left || 0;
        this.memoryInfoDiv.style.top = savedPosition.top || 0;
        this.memoryInfoDiv.style.right = 'auto';
        this.memoryInfoDiv.style.bottom = 'auto';
      }
    }

    initKeyboardEvents() {
      document.addEventListener('keydown', (event) => {
        if (event.key === 'F10') {
          event.preventDefault();
          if (this.memoryInfoDiv.style.display === 'none') {
            this.init();
          } else {
            this.destroy();
          }
        }
      });
    }
  }

  // 初始化监控器
  new MemoryMonitor();
})();