Skip to content

上传进度计算-平滑窗口算法

题目

已知:

  1. 总文件大小:totalSize 比特

  2. 已经上传的大小(实时 progress 进度,不固定时间更新) 比特

求:根据:totalSize、更新的时间和已经上传的大小,得出每秒上传的速度单位(比特),剩余的时间(毫秒)

提示:你可以使用平滑窗口算法得出过去的几次的平均上传速度,请使用 JavaScript 把代码写出来

答案

你可以通过平滑窗口算法来计算过去几次的平均上传速度,并基于此计算剩余时间。下面是一个使用 JavaScript 实现的示例代码:

javascript
class UploadSpeedCalculator {
  constructor(windowSize = 5) {
    this.windowSize = windowSize; // 滑动窗口大小
    this.progressRecords = []; // 记录进度变化
    this.timeRecords = []; // 记录时间变化
    this.speeds = []; // 初始化速度记录数组
  }

  // 更新进度,并计算上传速度
  updateProgress(uploadedBytes, currentTime) {
    // 如果已经有记录,计算当前上传速度
    if (this.progressRecords.length > 0) {
      const previousBytes =
        this.progressRecords[this.progressRecords.length - 1];
      const previousTime = this.timeRecords[this.timeRecords.length - 1];

      const bytesDiff = uploadedBytes - previousBytes;
      const timeDiff = currentTime - previousTime;

      if (timeDiff > 0) {
        const speed = bytesDiff / (timeDiff / 1000); // 每秒上传的速度,单位:比特/秒
        this.addRecord(speed);
      }
    }

    // 更新进度和时间记录
    this.progressRecords.push(uploadedBytes);
    this.timeRecords.push(currentTime);

    // 保证窗口大小不超过设定的windowSize
    if (this.progressRecords.length > this.windowSize) {
      this.progressRecords.shift();
      this.timeRecords.shift();
    }
  }

  // 添加记录,并保证滑动窗口大小
  addRecord(speed) {
    if (this.speeds.length >= this.windowSize) {
      this.speeds.shift(); // 移除最早的记录
    }
    this.speeds.push(speed);
  }

  // 获取平均上传速度
  getAverageSpeed() {
    if (this.speeds.length === 0) return 0;
    const totalSpeed = this.speeds.reduce((sum, speed) => sum + speed, 0);
    return totalSpeed / this.speeds.length;
  }

  // 计算剩余时间
  calculateRemainingTime(totalSize, uploadedBytes) {
    const averageSpeed = this.getAverageSpeed();
    if (averageSpeed === 0 || uploadedBytes === 0) return Infinity; // 防止除以0的情况

    const remainingBytes = totalSize - uploadedBytes;

    // 如果剩余字节数小于或等于0,则表示上传完成,返回0
    if (remainingBytes <= 0) {
      return 0;
    }

    return (remainingBytes / averageSpeed) * 1000; // 返回毫秒
  }
}

// 示例使用
const totalSize = 100000000; // 假设总文件大小为100MB
const calculator = new UploadSpeedCalculator(5);

// 模拟上传过程,实时调用updateProgress更新进度
function simulateUpload() {
  let uploadedBytes = 0;
  const interval = setInterval(() => {
    const currentTime = Date.now();
    uploadedBytes += Math.random() * 10000000; // 模拟上传
    if (uploadedBytes > totalSize) {
      uploadedBytes = totalSize; // 避免超过总大小
    }

    calculator.updateProgress(uploadedBytes, currentTime);

    // 确保已经有足够的进度数据用于计算
    if (calculator.speeds.length > 0) {
      console.log(
        `当前上传速度:${calculator.getAverageSpeed().toFixed(2)} bits/sec`
      );
      console.log(
        `预计剩余时间:${calculator
          .calculateRemainingTime(totalSize, uploadedBytes)
          .toFixed(2)} 毫秒`
      );
    } else {
      console.log("正在计算上传速度...");
    }

    if (uploadedBytes >= totalSize) {
      clearInterval(interval);
      console.log("上传完成");
    }
  }, 100);
}

simulateUpload();

代码说明:

  1. UploadSpeedCalculator 类中包含上传速度的计算和剩余时间的估算逻辑。
  2. updateProgress 方法会在上传进度发生变化时调用,用来更新当前的上传速度并维护一个滑动窗口大小的记录。
  3. getAverageSpeed 方法会计算过去几次的平均速度。
  4. calculateRemainingTime 方法会根据平均速度和剩余大小计算预估的剩余时间。
  5. simulateUpload 函数模拟了一个上传过程,周期性地更新上传进度并输出当前的上传速度和预计剩余时间。

你可以根据需要调整滑动窗口的大小来平滑速度波动。

算法优化,对方法进行防抖 getAverageSpeed calculateRemainingTime,如果在 1000 毫秒内请求将会返回上一个值

为了实现防抖机制并确保在 1000 毫秒内多次调用 getAverageSpeedcalculateRemainingTime 时返回上一个值,可以引入一个简单的防抖实现。通过记录上一次调用的时间和结果,如果在 1000 毫秒内再次调用,就直接返回之前的结果,而不是重新计算。

以下是优化后的代码:

javascript
class UploadSpeedCalculator {
  constructor(windowSize = 5) {
    this.windowSize = windowSize; // 滑动窗口大小
    this.progressRecords = []; // 记录进度变化
    this.timeRecords = []; // 记录时间变化
    this.speeds = []; // 初始化速度记录数组
    this.lastAverageSpeed = null; // 上次计算的平均速度
    this.lastRemainingTime = null; // 上次计算的剩余时间
    this.lastSpeedCalcTime = 0; // 上次计算速度的时间戳
    this.lastTimeCalcTime = 0; // 上次计算剩余时间的时间戳
    this.debounceInterval = 1000; // 防抖间隔 1000 毫秒
  }

  // 更新进度,并计算上传速度
  updateProgress(uploadedBytes, currentTime) {
    // 如果已经有记录,计算当前上传速度
    if (this.progressRecords.length > 0) {
      const previousBytes =
        this.progressRecords[this.progressRecords.length - 1];
      const previousTime = this.timeRecords[this.timeRecords.length - 1];

      const bytesDiff = uploadedBytes - previousBytes;
      const timeDiff = currentTime - previousTime;

      if (timeDiff > 0) {
        const speed = bytesDiff / (timeDiff / 1000); // 每秒上传的速度,单位:比特/秒
        this.addRecord(speed);
      }
    }

    // 更新进度和时间记录
    this.progressRecords.push(uploadedBytes);
    this.timeRecords.push(currentTime);

    // 保证窗口大小不超过设定的windowSize
    if (this.progressRecords.length > this.windowSize) {
      this.progressRecords.shift();
      this.timeRecords.shift();
    }
  }

  // 添加记录,并保证滑动窗口大小
  addRecord(speed) {
    if (this.speeds.length >= this.windowSize) {
      this.speeds.shift(); // 移除最早的记录
    }
    this.speeds.push(speed);
  }

  // 获取平均上传速度(带防抖)
  getAverageSpeed() {
    const currentTime = Date.now();

    // 如果在防抖间隔内调用,返回上一次的计算结果
    if (currentTime - this.lastSpeedCalcTime < this.debounceInterval) {
      return this.lastAverageSpeed;
    }

    if (this.speeds.length === 0) {
      this.lastAverageSpeed = 0;
    } else {
      const totalSpeed = this.speeds.reduce((sum, speed) => sum + speed, 0);
      this.lastAverageSpeed = totalSpeed / this.speeds.length;
    }

    this.lastSpeedCalcTime = currentTime;
    return this.lastAverageSpeed;
  }

  // 计算剩余时间(带防抖)
  calculateRemainingTime(totalSize, uploadedBytes) {
    const currentTime = Date.now();

    // 如果在防抖间隔内调用,返回上一次的计算结果
    if (currentTime - this.lastTimeCalcTime < this.debounceInterval) {
      return this.lastRemainingTime;
    }

    const averageSpeed = this.getAverageSpeed();
    if (averageSpeed === 0 || uploadedBytes === 0) {
      this.lastRemainingTime = Infinity; // 防止除以0的情况
    } else {
      const remainingBytes = totalSize - uploadedBytes;

      // 如果剩余字节数小于或等于0,则表示上传完成,返回0
      if (remainingBytes <= 0) {
        this.lastRemainingTime = 0;
      } else {
        this.lastRemainingTime = (remainingBytes / averageSpeed) * 1000; // 返回毫秒
      }
    }

    this.lastTimeCalcTime = currentTime;
    return this.lastRemainingTime;
  }
}

export default UploadSpeedCalculator;

优化点:

  1. 防抖实现:在 getAverageSpeedcalculateRemainingTime 方法中增加了防抖逻辑。如果调用间隔在 1000 毫秒以内,直接返回上次计算结果。
  2. 状态缓存:引入 lastAverageSpeedlastRemainingTime 变量来缓存上次的计算结果。

这样可以有效减少不必要的重复计算,提高性能。

对 windowSize 进行优化,由传入的个数改为时间范围的数据,每一次 updateProgress 都会记录一下加入的时间,时间间隔使用 debounceInterval 参数即可,从而反应出来一个真实的数据

windowSize 从固定数量改为基于时间范围的滑动窗口可以更真实地反映上传速度变化。每次调用 updateProgress 时记录当前的时间戳,随后保留在特定时间范围内的数据点(比如 debounceInterval),这将确保速度计算基于真实的时间范围。

以下是基于时间范围而非数量的优化版本:

javascript
class UploadSpeedCalculator {
  constructor(debounceInterval = 1000) {
    // 默认5秒的滑动时间窗口
    this.debounceInterval = debounceInterval; // 滑动窗口时间范围,单位:毫秒
    this.progressRecords = []; // 记录进度变化 [{bytes, time}]
    this.speeds = []; // 初始化速度记录数组
    this.lastAverageSpeed = null; // 上次计算的平均速度
    this.lastRemainingTime = null; // 上次计算的剩余时间
    this.lastSpeedCalcTime = 0; // 上次计算速度的时间戳
    this.lastTimeCalcTime = 0; // 上次计算剩余时间的时间戳
  }

  // 更新进度,并计算上传速度
  updateProgress(uploadedBytes, currentTime) {
    // 移除超出时间窗口的数据
    this.progressRecords = this.progressRecords.filter(
      (record) => currentTime - record.time <= this.debounceInterval
    );

    // 如果已经有记录,计算当前上传速度
    if (this.progressRecords.length > 0) {
      const previousRecord =
        this.progressRecords[this.progressRecords.length - 1];
      const bytesDiff = uploadedBytes - previousRecord.bytes;
      const timeDiff = currentTime - previousRecord.time;

      if (timeDiff > 0) {
        const speed = bytesDiff / (timeDiff / 1000); // 每秒上传的速度,单位:比特/秒
        this.addRecord(speed);
      }
    }

    // 添加新记录
    this.progressRecords.push({ bytes: uploadedBytes, time: currentTime });
  }

  // 添加记录,并保证滑动窗口大小(按时间范围)
  addRecord(speed) {
    this.speeds.push(speed);

    // 移除超过时间窗口的数据
    const currentTime = Date.now();

    // 确保 progressRecords 的长度与 speeds 保持一致
    if (this.progressRecords.length !== this.speeds.length) {
      // 当长度不一致时,将移除最旧的数据以保持同步
      this.speeds = this.speeds.slice(-this.progressRecords.length);
    }

    this.speeds = this.speeds.filter((_, index) => {
      const record = this.progressRecords[index];
      return record && currentTime - record.time <= this.debounceInterval;
    });
  }

  // 获取平均上传速度(带防抖)
  getAverageSpeed() {
    const currentTime = Date.now();

    // 如果在防抖间隔内调用,返回上一次的计算结果
    if (currentTime - this.lastSpeedCalcTime < this.debounceInterval) {
      return this.lastAverageSpeed;
    }

    if (this.speeds.length === 0) {
      this.lastAverageSpeed = 0;
    } else {
      const totalSpeed = this.speeds.reduce((sum, speed) => sum + speed, 0);
      this.lastAverageSpeed = totalSpeed / this.speeds.length;
    }

    this.lastSpeedCalcTime = currentTime;
    return this.lastAverageSpeed;
  }

  // 计算剩余时间(带防抖)
  calculateRemainingTime(totalSize, uploadedBytes) {
    const currentTime = Date.now();

    // 如果在防抖间隔内调用,返回上一次的计算结果
    if (currentTime - this.lastTimeCalcTime < this.debounceInterval) {
      return this.lastRemainingTime;
    }

    const averageSpeed = this.getAverageSpeed();
    if (averageSpeed === 0 || uploadedBytes === 0) {
      this.lastRemainingTime = Infinity; // 防止除以0的情况
    } else {
      const remainingBytes = totalSize - uploadedBytes;

      // 如果剩余字节数小于或等于0,则表示上传完成,返回0
      if (remainingBytes <= 0) {
        this.lastRemainingTime = 0;
      } else {
        this.lastRemainingTime = (remainingBytes / averageSpeed) * 1000; // 返回毫秒
      }
    }

    this.lastTimeCalcTime = currentTime;
    return this.lastRemainingTime;
  }
}

export default UploadSpeedCalculator;

主要变化:

  1. 滑动时间窗口:不再基于固定数量的记录,而是基于设定的时间范围(debounceInterval),确保只保留在该时间范围内的数据。
  2. 动态过滤记录:通过 updateProgressaddRecord 方法过滤出当前时间窗口内的记录。
  3. 防抖处理:继续使用防抖机制,确保在 debounceInterval 内重复调用 getAverageSpeedcalculateRemainingTime 返回缓存值。

优点:

  • 通过时间窗口进行优化,能更灵活地反映当前实际的上传速度。
  • 随着网络波动,速度变化更加精确。