【问题标题】:Firebase too slow to load for a tile-based gameFirebase 对于基于图块的游戏加载速度太慢
【发布时间】:2016-01-02 04:38:50
【问题描述】:

我正在使用 swift 和 Firebase 为 iOS 构建一个基于图块的 2d 游戏。因为世界很大,所以我将其设计为只订阅屏幕上的图块。也就是说,我没有为所有 10,000x10,000 块添加侦听器,而是将它们添加到屏幕上的块中。随着玩家的移动,我取消注册旧听众并注册新听众。我在屏幕边缘添加了一些缓冲区,希望在屏幕上移动时所有内容都能充分加载。不幸的是,Firebase 通常存在足够大的延迟,以至于这种策略根本行不通。在次优的互联网连接上,有可能继续走进“未加载的世界”,有时需要几秒钟来加载丢失的图块。

问题是这样的:同一连接和同一设备上的其他 MMO iOS 游戏运行良好。这不是一个可怕的联系。这让我怀疑我的实现,或者 Firebase 本身有问题。

基本上,每次我迈出一步时,我都会等待大约 20 个图块的“加载一次”事件。一个步骤大约需要 1/4 秒,所以我每秒从 Firebase 请求大约 100 个项目。不过,我想不出更好的方法。 Firebase 文档表明这应该不是问题,因为它都是一个套接字连接。我可以将对象“存储”到 10x10 块中,这意味着我将订阅更少的对象,但这在总数据传输方面也会更加浪费。如果套接字连接真的优化了,总的数据传输应该是唯一的瓶颈,这意味着这个策略是错误的。

编辑

这是一个展示其工作原理的视频。缓冲区大小已减小到-1,因此您可以轻松查看屏幕边缘以及加载和卸载的瓷砖。在视频快结束时,滞后来袭,我徘徊在空虚中。我打开了另一个游戏,它几乎立即加载。 http://forevermaze.inzania.com/videos/FirebaseLag.movn.b.,我在屏幕再次加载之前结束了录制。它永远不会加载失败,所以并不是代码无法工作。这纯粹是延迟。

这是我用来加载图块的代码。它为每个图块调用一次。正如我所说,这意味着该代码每步被并行调用大约 20 次。所有其他应用程序都以良好的速度运行,没有延迟。我在东京使用具有 LTE 连接的 MiFi,因此连接稳定。

  /**
   * Given a path to a firebase object, get the snapshot with a timeout.
   */
  static func loadSnapshot(firebasePath: String!) -> Promise<FDataSnapshot?> {
    let (promise, fulfill, _) = Promise<FDataSnapshot?>.pendingPromise()
    let connection = Firebase(url: Config.firebaseUrl + firebasePath)
    connection.observeSingleEventOfType(.Value, withBlock: { snapshot in
      if !promise.resolved {
        fulfill(snapshot)
      }
    })
    after(Config.timeout).then { () -> Void in
      if !promise.resolved {
        DDLogWarn("[TIMEOUT] [FIREBASE-READ] \(firebasePath)")
        fulfill(nil)
        //reject(Errors.network)
      }
    }
    return promise
  }

图块位于[ROOT]/tiles/[X]x[Y]。大多数图块包含非常少的数据,但如果该图块上有对象(即其他玩家),则会存储这些对象。这是 Firebase 的屏幕截图:

edit2

根据要求,我非常简单地重新创建了这个问题。这是一个 100 行的 XCTestCase 类:http://forevermaze.com/code/LagTests.swift

用法:

  1. 将文件拖放到您的 Swift 项目中(它应该是独立的,只需要 Firebase)
  2. firebaseUrl 的值更改为您的根 URL(即 https://MyProject.firebaseio.com
  3. 运行一次testSetupDatabase() 函数测试以设置数据库的初始状态
  4. 运行testWalking() 函数来测试延迟。这是主要测试。如果任何图块的加载时间超过 2 秒,它将失败。

我已经在几个不同的连接上尝试过这个测试。一流的办公室连接没有问题,但即使是高端 LTE 或 MiFi 连接也会失败。 2 seconds 已经是一个 非常 长的超时时间,因为这意味着我需要有一个 10 tile 缓冲区(0.2 秒 * 10 块 = 2 秒)。这是我连接到 LTE 连接时的一些输出,显示加载磁贴花了将近 10 秒(!!): error: -[ForeverMazeTests.LagTests testWalking] : XCTAssertTrue failed - Tile 2x20 took 9.50058007240295

【问题讨论】:

  • 感谢您对目标的清晰描述。但这是一个相当广泛的话题。如果不把它煮沸一点,就很难有多大帮助。您能否将问题简化为特定的 sn-p 代码、数据结构示例以及关于两者的问题?
  • @FrankvanPuffelen 谢谢。我添加了一堆附加信息,包括视频、代码 sn-ps 和我的 Firebase 数据树的屏幕截图。
  • 仅供参考 该视频无法为我播放;也许它已损坏。
  • @jtbandes 抱歉,我在save edit 按钮上有点快。现在刚刚上传完毕,请重试(我测试过,对我有用)。
  • 该代码看起来不错,数据结构看起来也足够小。因此,在这个 sn-p 的背景下,更有可能是其他原因导致了减速。您能否将重现问题的程序减少到最小(但完整),以便有人可以在本地运行和重现问题?在这种情况下,如果您将 JSON 作为文本而不是屏幕截图包含在内,也会有所帮助。

标签: swift firebase


【解决方案1】:

当我通过 3G 连接进行测试时,我运行了一些测试并在 15-20 秒内完成加载。在我的常规连接中,它需要 1-2 秒,因此差异可能完全取决于带宽。

我将您的测试用例重写为 JavaScript 版本,因为我很难弄清楚发生了什么。在这里找到我的:http://jsbin.com/dijiba/edit?js,console

var ref = new Firebase(URL);
var tilesPerStep = 20;
var stepsToTake = 100;

function testWalking() {
  var startTime = Date.now();
  var promises = [];
  for (var x=0; x < stepsToTake; x++) {
    promises.push(testStep(x));
  }
  Promise.all(promises).then(function() {
    console.log('All '+promises.length+' steps done in '+(Date.now()-startTime)+'ms');
  });
}

function testStep(x) {
  var result = new Promise(function(resolve, reject){
    var tiles = ref.child("/tiles_test");
    var loading = 0;
    var startTime = Date.now();
    console.log('Start loading step '+x);

    for (var y=0; y < tilesPerStep; y++) {
      loading ++;
      tiles.child(x+'x'+y).once('value', function(snapshot) {
        var time = Date.now() - startTime;
        loading--;
        if (loading === 0) {
          console.log('Step '+x+' took '+(Date.now()-startTime)+'ms');
          resolve(Date.now() - startTime);
        }
      });
    }
  });
  return result;
}

testWalking();

最大的区别是我不会延迟开始任何加载,也不会因特定图块而失败。我认为最后一点是您的测试失败的原因。

从 Firebase 的所有加载都是异步进行的,但所有请求都通过同一个连接。当你开始加载时,你会排队很多请求。此时间因“尚未完成的先前请求”而出现偏差。

这是一个仅包含 10 个步骤的测试运行示例:

"Start loading step 0"
"Start loading step 1"
"Start loading step 2"
"Start loading step 3"
"Start loading step 4"
"Start loading step 5"
"Start loading step 6"
"Start loading step 7"
"Start loading step 8"
"Start loading step 9"
"Step 0 took 7930ms"
"Step 1 took 7929ms"
"Step 2 took 7948ms"
"Step 3 took 8594ms"
"Step 4 took 8669ms"
"Step 5 took 9141ms"
"Step 6 took 9851ms"
"Step 7 took 10365ms"
"Step 8 took 10425ms"
"Step 9 took 11520ms"
"All 10 steps done in 11579ms"

您可能会注意到,每个步骤所花费的时间并不等于所有步骤所花费的时间。本质上,您正在启动每个请求,而管道中仍有请求。这是加载这些项目的最有效方式,但这确实意味着您需要以不同的方式衡量性能。

基本上所有步骤开始几乎在同一时间。然后您正在等待第一个响应(在上述情况下,包括建立从客户端到正确 Firebase 服务器的 WebSocket 连接),然后响应以合理的间隔出现(假设每个步骤有 20 个请求)。

所有这些都很有趣,但它当然不能解决您的问题。我建议您将数据建模为屏幕大小的存储桶。因此,不要将每个图块分开,而是将每 10x10 个图块存储在一个“桶”中。您将减少每个单独请求的开销,并且每 10 步最多只需要请求一个存储桶。

更新

我很确定我们只是在调试您的基准测试方法的多个工件。如果我将代码更新为:

func testWalking() {
    let expectation = expectationWithDescription("Load tiles")
    let maxTime = self.timeLimit + self.stepTime * Double(stepsToTake)

    let startTime = NSDate().timeIntervalSince1970

    for (var x=0; x<stepsToTake; x++) {
        let delay = Double(x) * stepTime
        let data = ["x":x, "ex": expectation]
        stepsRemaining++
        NSTimer.scheduledTimerWithTimeInterval(0, target: self, selector: Selector("testStep:"), userInfo: data, repeats: false)
    }
    waitForExpectationsWithTimeout(maxTime) { error in
        let time = NSDate().timeIntervalSince1970 - startTime
        print("Completed loading after \(time)")
        if error != nil {
            print("Error: \(error!.localizedDescription)")
        }
    }
}

/**
* Helper function to test a single step (executes `tilesPerStep` number of tile loads)
*/
func testStep(timer : NSTimer) {
    let tiles = Firebase(url: firebaseUrl).childByAppendingPath("/tiles_test")
    let data = timer.userInfo as! Dictionary<String, AnyObject>
    let x = data["x"] as! Int
    let expectation = data["ex"] as! XCTestExpectation
    var loading = 0
    print("Start loading \(x)")

    for (var y=0; y<tilesPerStep; y++) {
        loading++
        tiles.childByAppendingPath("\(x)x\(y)").observeSingleEventOfType(.Value, withBlock: { snapshot in
            loading--
            if loading == 0 {
                print("Done loading \(x)")
                self.stepsRemaining--
                if self.stepsRemaining == 0 {
                    expectation.fulfill()
                }
            }
        })
    }
}

在高速网络上不到 2 秒即可完成整个加载,在 3G 上则需要 15 到 25 秒。

但我建议的建模水平不只是单个图块。

【讨论】:

  • 我对你的评论有点困惑:我的方法中的工件。如果您查看我的代码,您会注意到XCAssertTrue 代码是按步骤运行的。也就是说,即使在触发每个步骤时存在延迟,也可以通过断言没有步骤花费超过 1/5 秒(整个步骤序列花费一定的时间来计算期望值)时间量;我认为您可能对waitForExpectationsWithTimeout 函数感到困惑,这实际上更像是“最终超时”,与单个测试无关)。我不关心总时间,只关心步数。
  • 您将 100 个步骤(每个步骤 20 次读取)排入数据库,该数据库按顺序处理它们(并在线上对它们进行管道传输)。除非您正确序列化请求(在前一个步骤完成后开始每个下一步或限制请求速率以匹配响应速率),否则后面的步骤肯定会花费更长的时间,因为数据库仍在处理它之前的步骤中的请求.
  • 我在jsbin(在testWalking2() 方法中)添加了一个“正确序列化请求”的示例。现在,每个步骤仅在前一个步骤完成后才开始。现在在 3G 上,每一步(20 次读取)需要 1 到 2 秒,总运行(100 步)需要 181 秒。因此,尽管总时间远高于我之前得到的 20 秒,但每个步骤的测量现在是准确的,因为步骤不再受到来自先前步骤的待处理请求的影响。
  • 不过,您的代码完全不同。第一步完成后才迈出第二步,意味着玩家无法顺利行走。看看我的视频。想象一下,如果第一步需要 0.3 秒才能完成。这意味着玩家将被冻结 0.1 秒,直到第二步可以触发。如果任何一个图块明显滞后(例如我的 10 秒示例),它会冻结整个游戏。
  • 我添加了两种类型的代码:testWalking() 与您的相同,但删除了时间。 testWalking2() 一个接一个地执行这些步骤。但我认为我们已经花了足够的时间讨论微优化:正确的解决方案是按块加载图块,而不是按图块加载。
猜你喜欢
  • 1970-01-01
  • 2014-01-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-30
  • 1970-01-01
相关资源
最近更新 更多