Giter Club home page Giter Club logo

bugs's People

Contributors

smallnewer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

bugs's Issues

mongodb设置整数时,最终变成小数的问题

在shell修改文档,如:{"$set":{"skillcoin":4},最终变成了Double类型。

在shell中,mongo默认把数字转成floating-point类型。所以我们需要把类型指定为我们需要的,譬如NumberInt或NumberLong。http://docs.mongodb.org/manual/core/shell-types/

参见:http://stackoverflow.com/questions/8218484/mongodb-inserts-float-when-trying-to-insert-integer

答案:

db.data.update({'name': 'zero'}, {'$set': {'value': NumberInt(0)}})

chromium 38中文字模糊的问题

发生场景:crosswalk 9.x 内置的chromium38.
网上说PC chrome也有类似,不过没有复现。可能已经修复。不过crosswalk内仍然有。

文字模糊我遇到两个情况:
第一:比较诡异,父标标签absolute,子两个标签都absolute。但一个指定z-index,一个不指定。此种情况会发生一个模糊一个不模糊的BUG。指定z-index的会模糊。怀疑是chrome走了硬件加速渲染,导致的模糊。(文字模糊的根本原因就是硬件加速渲染下的问题。)
第二:用aniamtion+transform做div的动画。不算是BUG。由于定义了animation-fill-mode: both;,导致div结束时仍然应用了transform,从而模糊。解决办法:不用fill-mode,或者在div身上用transform:none;覆盖掉。

资源服务器的搭建和资源管理方案

资源服务器的整体思路

整体思路是,线上搭建SVN服务器用来管理所有开发过程中的资源,本地checkout一份用来上传和替换资源。服务器上checkout两份,一份用于测试(用nginx一个端口指向这个目录),另一份用于正式的发布。都放到线上开发的时候可能会比较卡,所以我本地搭建一个虚拟主机,再checkout一份,用于开发过程中调试使用。

资源管理方式

  1. 需要用到的资源先在本地目录中跑一遍md5重命名脚本(nodejs),将新添加或者要替换的文件导出一份放到svn的目录下,然后上传。
  2. 开发过程虚拟主机端需要update,才能访问最新资源,如果有测试版本要更新,再到线上update
  3. 准备发布后将正式目录update一下就行了

资源目录(方便记录)

  • SVN:/home/svn/project/xhb
  • release-repository: /home/svn/release-repository
  • test-repository: /home/svn/test-repository
  • nginx: /usr/local/nginx

本地资源上传处理

  1. 先复制一份res_server到backup_res_server,将其完全覆盖
  2. 将backup_res_server上未添加SHA1的加上
  3. 将backup_res_server文件选择性复制到out_res_server中一份

用了一个模块 fs-extra

配置本地虚拟主机

系统: centos 6.5
IP:

记录一下搭建过程

nginx 安装(安装包形式)

1.安装make

yum -y install gcc automake autoconf libtool make

2.安装g++

yum install gcc gcc-c++

3.找一个源码目录

 cd /usr/local/src 

4.安装PCRE库(重写rewrite)

cd /usr/local/src
wget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.36.tar.gz 
tar -zxvf pcre-8.36.tar.gz
cd pcre-8.36
./configure
make
make install

5.安装zlib库(用于gzip压缩)

cd /usr/local/src

wget http://zlib.net/zlib-1.2.8.tar.gz
tar -zxvf zlib-1.2.8.tar.gz
cd zlib-1.2.8
./configure
make
make install

6.安装ssl

cd /usr/local/src
wget http://www.openssl.org/source/openssl-1.0.1c.tar.gz
tar -zxvf openssl-1.0.1c.tar.gz

7.安装nginx

cd /usr/local/src
wget http://nginx.org/download/nginx-1.4.2.tar.gz
tar -zxvf nginx-1.4.2.tar.gz
cd nginx-1.4.2

./configure --sbin-path=/usr/local/nginx/nginx --conf-path=/usr/local/nginx/nginx.conf --pid-path=/usr/local/nginx/nginx.pid --with-http_ssl_module --with-pcre=/usr/local/src/pcre-8.36 --with-zlib=/usr/local/src/zlib-1.2.8 --with-openssl=/usr/local/src/openssl-1.0.1c

make
make install

--with-pcre=/usr/src/pcre-8.36 指的是pcre-8.36 的源码路径。
--with-zlib=/usr/src/zlib-1.2.7 指的是zlib-1.2.7 的源码路径。

8.确保系统的 80 端口没被其他程序占用

netstat -ano|grep 80

回头测试端口和正式端口要分开

9.启动吧,ng!!

sudo /usr/local/nginx/nginx

单单做静态资源用是可以了,接下来就需要配置一下啦!

nginx 配置

我想要nginx通过不同的端口指向不同的目录;暂定80指向release-repository目录。先给test-repository分配一个端口

  1. 修改nginx.conf文件
#在server中,修改root的指向目录
root /home/svn/test-repository;

#在server同级别结构中,引入我自定义的conf文件.
#后续所有自定义的conf文件都放到custom-conf中
include  /usr/local/nginx/custom-conf/*.conf;
  1. 在custom-conf新建test-repository.conf文件
vi test-repository.conf

server {
    listen       8000;
    server_name  localhost;
    root /home/svn/test-repository;

    #charset koi8-r;
    #access_log  logs/host.access.log  main;

    location / {
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }       
}
  1. 重启nginx
cd /usr/local/nginx
./nginx -s reload

好了就可以了

每日次数清零的实现

两种情况

  1. 单纯的清零,不需要展示到前端。
  2. 需要和前端展示统一。

已竞技场为例,每日定点刷新,计算排名、然后清除每日完成的次数。这两个操作都比较耗时。是保证用户在两个操作完成之前,看到的都是统一的未重置的情况,还是允许用户先看到排名更新、奖励发放,再过几分钟看到竞技场任务重置。

清除每日完成次数有对应两种实现方式:
第一种简单,前后端也无法统一。就是TTL集合。插入的时候算好时间,每日定点更新。开发起来非常简单。
缺点:索引多了一个,插入慢,另外每日清空,速度必然慢(应该类似remove)。如果日活有限,影响不大
优点:开发不需要写后台运行脚本,不需要维护其他逻辑。

第二种,写一个每日运行的脚本。定点遍历表所有的内容,进行更新。因为是固定的清零(不是重置为不同的值),所以update的效率应该比remove要高得多。
缺点:需要单独开发定时任务,还需要进行监控。更新时,要更新大量不活跃用户的数据,浪费时间
优点:比第一种快,比第三种容易理解
至于更新大量不活跃用户的数据,可以考虑每周手工清理一次死账号,即一周都没登陆的人,不是删账号,而是把这种日更新的表关于他们的记录删除,提高每日更新的速度。
或者不用update,直接用dropCollection非常快。当用户要获取数据时,发现没有再insert一下即可。好处是:没有冗余数据。坏处是insert会导致获取数据变慢。需要注意瞬间大量的insert导致mongodb瘫痪。

第三种,懒更新,记录用户每个人的当日数据和最后一次更新时间。每次获取数据前,都判断一下最后更新时间是否是昨日以前的,是的话把数据重置。
缺点:每次获取都需要额外取一下相关时间,降低平时的效率;不易理解,维护不易,新进的人开发新接口容易出现BUG。最关键的是第二点,怕出现BUG影响整体。
优点:没有每日批量更新的时间。

第一种,无法保证事务性,即更新排名+重置肯定会在不同的时间完成,且前端会看到,一个先完成,一个后完成。

第二种,可以在此基础上,加上事务的特性,保证前端看起来一致。

综合下来,第一种应该最简单,在表的数据不大时,应该是性价比最高的。
第二种可以承受更大的数据量,但开发运维工作量会多
第三种不建议采用,维护性较差。单人项目可以用用。多人维护最好还是放弃。

[测试]索引大小

测试1-用数字做索引

代码:

var maxlen = 100 * 10000;
var arr = [];
for (var i = 0; i < maxlen; i++) {
    arr.push({
        _id: i,
        jjc: maxlen - i
    });
};
db.a.insert(arr);

没插入完shell就崩了,没深究,插入了81万多点。_id的索引大小为22868272byte,约为21M,之后为jjc字段加了索引,大小为20619872byte。

100万的文档数量,单键索引在30M以内的话,完全可以接受。之前网上有测试,100万数字做索引,索引要300M,有点难以接受。

4.22 更新 。从2.6升级到3.0后,100万_id字段的int索引,只有12M大小,比2.6小很多。但是,貌似update的群更操作变慢许多,100W的已存字段更新需要19秒。之前应该是4秒左右

测试2-用字符串做索引

var maxlen = 100 * 10000;
var arr1 = [];
for (var i = 0; i < maxlen; i++) {
    arr1.push({
        _id: i + "",
        jjc: maxlen - i
    });
};
db.b.insert(arr1);

这次仍然是没插入到100万,只有68万。_id索引大小为31289552byte,可以看出绝对比int大了。jjc仍然为17177776byte,和测试1是能对应上的。

ObjectId就不测试了,在项目中没有特别的用处,在客户端中还得包装成类,不如干脆用自增的int代替。节省热数据占用内存的大小。

mongodb的nodejs驱动问题记录

测试环境为node 0.12.0,查找82万文档的集合。

测试代码:

var MongoClient = require('mongodb').MongoClient;

var url = 'mongodb://xxxx:27017/test1';
MongoClient.connect(url, function(err, db) {

    if (err) {
        return console.log("数据库链接失败");;
    };

    console.log("数据库链接成功");

    var ind = 0;

    db.collection("a", function (err, col) {
        var cursor = col.find();

        console.time("find all");
        cursor.nextObject(next);

        function next (err, doc) {

            if (doc === null) {
                console.timeEnd("find all");
                console.log("over");
                return;
            };

            ind++;
            if (ind % 10000 === 0) {
                console.log(ind)
            };
            setImmediate(_next);  // in 1.4.x
            // cursor.nextObject(next); // in 2.0.x
        }

        function _next () {
            cursor.nextObject(next);
        }
    });
})
  • mongodb驱动现在发布了2.0系列,暂时不知道和老版的区别。

老版中(测试的1.4.31),用nextObject遍历,会出现内存泄露。不知道是驱动故意缓存,还是忘了释放。除此之外,nextObject应该用了nextTick进行迭代,会报堆栈溢出的错误。迭代现在node推荐用setImmediate实现。

在2.0系列中(测试的是2.0.16),没有内存泄露,一直稳定在36M.且不需要借用setImmediate.

在观察ind的变量输出时,能明显感觉到2.0输出的要比1.4输出的快很多,更加流畅。

  • mongoose在4.0.0-rc系列中已经使用了2.0.14,如果有必要,将来换到4.0版本。

81W文档find出来,node和mongo同一台机器,用了6.5s。网络出流量3m,内网完全扛得住。cpu跑满。
将来可以通过提高cpu核数提高获取速度。
千万级文档下,限制主要看redis的写入速度。按照1W/S插入速度,百万文档耗时100秒,千万就需要10分钟。按照非常理想的10W/S插入速度,千万级文档也只需要1分钟。

mongodb驱动的重连BUG

如果是驱动端的网络出了问题,不会立马触发disconnected事件。只有当驱动端尝试查询一个操作,得到链接失败的错误后,才会触发disconnected事件。之后才尝试重连。

用的mongoose 3.8+系列。4.0还没试。4.0改用了官方驱动2.0+版,不知解决这个问题没有。

将CSV导入mongodb

先说下导出:

./mongoexport -h "127.0.0.1:27017" -d "库名" -c "集合名" -f "_id,name" --csv -o "aaa.csv"

接下来是导入:

./mongoimport -h "127.0.0.1:27017" -d "苦命" -c "集合名" --file aaa.csv --type csv --headerline

导入时注意:

  1. csv行和列不能有多余的,否则会导入多余数据,甚至报错
  2. csv貌似必须有_id字段
  3. --headerline指第一行是列名。

小米手机中fixed定位gif,gif不播放

产生条件:

  1. 小米2和小米2s微信中;(小米3未验证)
  2. gif所在标签fixed定位;
  3. display:none;block;切换(待确定)

解决方法:
修改position为非fixed

影响cocos性能的东西

通用:
像素密度。 6 plus 带不动3倍密度

webgl:
V3.5 webgl在非骨骼动画上,要比canvas流畅许多。不只是帧数。如大场景用scrollView,即便canvas是60帧,看着也有卡顿。再如骨骼动画,即便canvas帧数高,看着动画也是一顿一顿的。

canvas
iOS上,QQ浏览器的渲染要比Safari强多,跟之前Safari的canvas表现一样。还没确定到原因,期间iOS系统版本变了,cocos的版本也从3.2 3.3到了3.5。之前测的Safari canvas挺流畅。

generator与promise创建性能

console.time("gen");
for (var i = 0; i < 5000; i++) {
    d();
};
console.timeEnd("gen");


console.time("promise");
for (var i = 0; i < 5000; i++) {
    new Promise(function () {

    });
};
console.timeEnd("promise");

结果:

gen: 1ms
promise: 9ms

按每秒5000处理、每个处理需要10个promise估算,每秒只创建promise就需要90ms,占将近1/10.
实际中,用co测thunk和promise的响应HTTP能力,也大约在1/10左右。

promise的创建还是太重,需要慎重。

cocos中类扩展的坑

这个真是坑啊,无语了。
通常在模拟类的实现时,属性是放在实例上,方法是放在原型上,取了节省性能和互不干扰的折中。但是cocos的有点扯。属性也放在了原型上,这就导致了一些问题。
先看重现:

// 定义一个新的类
var newClass = cc.Class.extend({
    prop: 123,
    ctor: function(){
        this.super();
        this.prop = 456;
        console.log(this.prop);    // 456
        console.log(this.__proto__.prop)   // 123
    }
});
// 想想,在这里定义prop的时候,下意识是不是认为这是给newClass的实例的?而不是给newClass本身的?

这个坑在平时可能不起眼~看看是怎么发生的:

var ClassA = cc.Class.extend({
    arr: [],
    ctor: function(){
        this.super();
    },
    init: function(){
        this.arr.push(1);
    },
    onExit: function(){
        this.arr = [];
    }
});
// 假设这个对象一直存在,要不停的被复用。
var obj1 = new ClassA();
obj1.init();
// 过一会后
obj1.onExit();
// 再过一会
var obj2 = new ClassA();
obj2.init();
// 此时输出`obj2.arr`会发现是`[1, 1]`,而不是期望的`[1]`

尽管我们在onExit时释放了this.arr,让他等于一个新数组。但原型上的arr仍然存在。当实例一个新的对象obj2时,obj2.arr访问的仍然是原型上的arr,结果就变成了[1, 1]

可能会认为在ctor中初始化一下this.arr = []不就结了,实际上ES6的class就是这么要求的。cocos为我们提供的这个类扩展方式,很容易产生歧义。

看看ES6的class规范如何定义属性:

class Point {

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '('+this.x+', '+this.y+')';
  }

}

属性要求用this.的方式创建。这样属性定义在实例身上,原型自然干净,坑就更少了。

不过cocos采用的貌似是jQuery作者提出的继承方案。是当初没有class时模拟class的一个方案。这种定义属性的方式,很容易JS原生开发者产生歧义。

再拿PHP的例子看看,虽然不确定PHP具体细节,但至少执行上看起来是没有歧义的:

class Test 
{
    public $a = array();

    function __construct()
    {   
    }
}

$obj = new Test();
array_push($obj->a, "1");
array_push($obj->a, "2");
array_push($obj->a, "3");
var_dump($obj->a);    // array(3) { [0]=> string(1) "1" [1]=> string(1) "2" [2]=> string(1) "3" }

$obj1 = new Test();

var_dump($obj1->a);    //  array(0) { } 

可以看到obj1对属性a的修改并没有影响到obj2的属性a。但是翻译到cocos的extend,就会出现最上面提到的问题。所以要么就完整的支持 类似PHP这种 直接定义属性 的方式,要么就像ES6的class一样,不支持,只允许this.方式创建属性。

不吐槽了。

结论

属性放在ctor函数内进行用this.prop = value形式声明。

是的,因为这个,我们遇到严重的内存泄露了。

参考文档

ES6中文学习文档 http://es6.ruanyifeng.com/#docs/class

nodejs中的http.Server事件[未完]

通常情况下,我们知道Node.js是这么创建一个httpServer的:

var http = require("http");
var server = http.createServer(function(req, res) {

});

之后要把server启动起来:

server.listen(8080);

在Node.js中,httpServer有这么几个事件:

  • Event: 'request'
  • Event: 'connection'
  • Event: 'close'
  • Event: 'checkContinue'
  • Event: 'connect'
  • Event: 'upgrade'
  • Event: 'clientError'

说一下其中比较常用且容易迷惑的:request、connection、close、connect这四个。

connection和connect

文档里说的很清楚

关于connection:

// 回调函数拥有一个socket参数
server.on("connection", function (socket) { })

When a new TCP stream is established. socket is an object of type net.Socket. Usually users will not want to access this event. In particular, the socket will not emit readable events because of how the protocol parser attaches to the socket. The socket can also be accessed at request.connection.
当一个新的TCP流被建立。 socket是一个net.Socket类型的对象。通常用户不会用到这个事件。由于协议方面的原因,参数socket不会触发readable事件。socket参数也可以通过request.connection访问到。

当一个新的TCP流被建立时,触发这个事件。connection是一系列事件中,最早触发的一个,此时,httpServer的监听函数还未开始执行。

connect

// 回调函数有三个参数
function (request, socket, head) { }

Emitted each time a client requests a http CONNECT method. If this event isn't listened for, then clients requesting a CONNECT method will have their connections closed.
* request is the arguments for the http request, as it is in the request event.
* socket is the network socket between the server and client.
* head is an instance of Buffer, the first packet of the tunneling stream, this may be empty.
After this event is emitted, the request's socket will not have a data event listener, meaning you will need to bind to it in order to handle data sent to the server on that socket.
当一个客户端使用CONNECT方法请求时触发。如果这个事件没有被监听,那么客户端的链接将被关闭。
* request 是一个http request,同样它也出现在request事件中
* socket 是server和客户端之间使用的网络socket
* head 是Buffer的一个实例,可能是空的。
在这个事件被触发后,请求的socket没有监听data事件,你需要手动监听一下,才能获取到发送到server上的数据。

通常我们更经常用connection事件。

request

这个容易和connection搞混。connection事件触发的更早。request的触发事件在server的回调函数(handle)执行完毕以后。

var http = require("http");
var server = http.createServer(function(req, res) {
    console.log("server handle");
});
server.listen(8080);

server.on("connection", function(){
    console.log("connection");
});

server.on("request", function(){
    console.log("request");
});

当触发一个请求时,输出的过程是:

connection
server handle
request

多提一句,request的触发时机不会受res的影响。无论在handle函数中有没有将res.end掉。

下面这段代码就会使request无法正常触发:

var http = require("http");

var server = http.createServer(function (req, res) {
        console.log("server handle");
    throw new Error();
});

process.on("uncaughtException", function () {})

server.listen(8080);

server.on("request", function () {
    console.log("request")
});

server.on("connection", function () {
    console.log("connection")
})

输出只有:

connection
server handle

看看官方文档对他的解释:

Emitted each time there is a request. Note that there may be multiple requests per connection (in the case of keep-alive connections).
每当有一个请求就会触发。注意:每个connection可能会触发多个request(比如使用keep-alive的长连接)

close

pomelo集群的一些特征

大体的执行流应该是:

  1. pomelo start
  2. node app.js
  3. master服务器启动
  4. 根据配置生成服务器:分本地和远程的流程
  5. 本地的,直接生成新的node启动命令
  6. 远程的,通过配置SSH,进行SSH远程执行
  7. 本地的服务器受master进程影响,master被干,所有本地服务器全消失;远程的应该不受影响,没测
  8. 所有remote服务器的代码,都会被connector服务器执行一次。也就是共执行了两次,巨扯淡。

cocos JS加载与合并的BUG

一共是两个问题。
cocos2d-js v3.5

JS加载

当cocos的各个JS文件没有被合并,使用其动态加载加载进来时。在Android X5浏览器上会有问题(其他浏览器没注意)。
表现为:加载到不定个数的JS文件时,就会抛出错误,提示加载失败。
解决办法就是在CCBoot.js中找到_createScript方法,在加载失败时,再次重试加载。可以进去。

JS文件打包发布后,巨卡的问题

iOS上没有明显表现。主要是Android上,表现为帧数很低。只有很低的次数表现帧数正常,只要一刷新,帧数立马从60掉到10-20左右。
暂时没有深究是何处的问题。
打包后巨卡的问题可以追踪V762版代码

cocos2d js 查看纹理内存占用情况

jsb 下可以用cc.textureCache.getCachedTextureInfo(); 然后在命令行里可以打印出来

网页下 是用cc.textureCache.dumpCachedTextureInfo(); 控制台直接可以看见

开发过程中的一些约定

人物

1. 骨骼动画

  • sheet.png
  • sheet.plist
  • sheet.json

2. 人物名称

使用人物ID

3. 动作名称

  • 站立 : idle
  • 移动 : run
  • 死亡 : dead
  • 被攻击 : underAttacked
  • 攻击1 : atk1
  • 攻击2 : atk2

4. 人物规格

按照最大尺寸场景的资源来制作,完成后再将png和plist等比缩小

场景

  1. 所有Sprite在移除场景的时候最好都执行一次cleanup

mongodb的多表插入的原子性-草稿

经常需要多表同时插入,譬如拿用户注册来说,用户有两个表,user和user_detail。一个存了用户的账号信息,一个存了用户更多的详细信息。这些分开的表往往因为其他原因无法合并在一起。
user我们认为是主表,其他的表都是user的延伸表,只有user存在了,其他表的数据才有意义。
当我们注册时,会在两个表里同时插入该用户对应的信息。跨表跨文档的操作都是非原子性的,对于用户注册这种重要性很高的一个功能,实现不当出现问题就比较麻烦。

插入顺序

要优先把附表的记录插入完毕,最后插入主表的记录。

假如用如下实现方式:

  1. 先插入user表的记录
  2. 再插入user_detail的记录
    如果客户端进程在user和user_detail中间崩溃了,就会出现user和user_detail不一致。当下次用户登录时,发现user表内有信息,可以正常登陆,但读取detail信息时就会出问题。更严重的是,一个表的信息不对甚至会影响其他更多表的数据正确性。

所以最好是反过来。先把延伸表们的记录插入玩,最后插入主表的信息。这样即便中间失败,用户也不会登陆成功,不会用到其他延伸表的数据。

注意:如果程序有些地方,如计划任务,定时刷新一些延伸表的信息,那就会导致那些主表失败延伸表OK的数据被更新。最终用户注册进来以后,发现延伸表的数据并非初始化的。

由于模拟表上的锁代价太大,所以在使用存在多表插入的表时,不能认为他们是可靠的,要考虑到不完整数据的情况。

ws模块性能

测试代码:

// server
var WebSocketServer = require('ws').Server
  , wss = new WebSocketServer({ port: 8080 });

ind = 0;
msg = "";
t = ""
wss.on('connection', function connection(ws) {
  ws.on('message', function incoming(message) {
    // console.log('received: %s', message);
    ind++;
      msg = message
      t = Date.now();
  });


});

setInterval(function () {
    console.log(msg.toString(),t);
}, 1000)
// client
var WebSocket = require('ws');
var ws = new WebSocket('ws://localhost:8080');

var  opt = {binary: true, mask: true}

ws.on('open', function open() {
  console.time("send");
  for (var i = 0; i < 10 * 10000; i++) {
    ws.send(Date.now()+"", opt);
  };
  console.timeEnd("send");
});


var isfirst = false;
var ind = 0;
ws.on('message', function(data, flags) {
  // flags.binary will be set if a binary data is received. 
  // flags.masked will be set if the data was masked. 
  // console.log(data)
  ind++;
  // console.log(ind)
});

mac pro。单机测试。平均下来每秒3000条左右的 客户端->服务器传输量。
效率实在太低了。不过这个测试也只是粗略做一下测试,并不适合Node.js之间的通信。
据说ws还是Node.js WebSocket模块中性能最好的。socket.io只有其一半。

Test

kklkklk
#12312

ddd

sdfasdf

alert(1)

#8

node的平滑重启

http://lostjs.com/2014/06/22/0-second-reload-for-node-web-server/

reload有时执行不成功,会退化成restart。还是推荐gracefulReload,手动处理。

进程收到'shutdown'消息后:

  1. 先server.close()拒绝所有的新链接。此时新链接会分配到新启动的进程
  2. 再停留5秒,为已接受的请求进行处理。对接口来说5秒足够了。
  3. 5秒后直接退出进程,重启完毕。

JS 性能tips

一些JS的方法有性能坑,平时可以不用管,在压榨性能的场景下,需要用到。此贴作为积累和记录。

如何复制一个集合

排行榜中需要用到这个功能,但是将来很可能不用mongdb做排行榜了,感觉不太合适。记录一下。

有几个方法可以复制一个集合
参考资料:http://stackoverflow.com/questions/8933307/clone-a-collection-in-mongodb

函数遍历

db.demo1.categories.find().forEach( function(x){db.demo2.categories.insert(x)} );

这里跑题一下。在shell中,find有些机制需要了解一下。摘自《MongDB权威指南(第二版)》。

调用 find 时,shell 并不立即查询数据库,而是等待真正开始要求获得结果时才发 送查询,这样在执行之前可以给查询附加额外的选项。几乎游标对象的每个方法都 返回游标本身,这样就可以按任意顺序组成方法链。例如,下面几种表达是等价的:

     > var cursor = db.foo.find().sort({"x" : 1}).limit(1).skip(10);
     > var cursor = db.foo.find().limit(1).sort({"x" : 1}).skip(10);
     > var cursor = db.foo.find().skip(10).limit(1).sort({"x" : 1});

此时,查询还没有真正执行,所有这些函数都只是构造查询。现在,假设我们执行 如下操作:

     > cursor.hasNext()

这时,查询被发往服务器。shell 立刻获取前 100 个结果或者前 4 MB 数据(两者之 中较小者),这样下次调用 next 或者 hasNext 时就不必再次连接服务器取结果了。 客户端用光了第一组结果,shell 会再一次联系数据库,使用 getMore 请求提取更 多的结果。getMore 请求包含一个查询标识符,向数据库询问是否还有更多的 结果,如果有,则返回下一批结果。这个过程会一直持续到游标耗尽或者结果全部 返回。

虽然不确定驱动中是否类似,不过驱动提供了和shell类似的语法,譬如nodejs中不是cursor.hasNext()而是cursor.nextObject(),不是forEach而是each。猜测应该是一样的原理。这样也就不需要担心同时把大量数据加载到nodejs导致进程崩溃的问题。

参考http://mongodb.github.io/node-mongodb-native/markdown-docs/queries.html#find-first-occurence-with-findone

回到主题,这个方法并不会造成写锁,就是普通的查询和插入。也就是无法复制瞬间的数据快照。(有人的测试数据是每秒2700条处理。想一下百万数据得多久)

官方给了一个更直观的方法。

copyTo

http://docs.mongodb.org/manual/reference/method/db.collection.copyTo/
不过有人提示:不要在生产环境使用,因为这东西会锁住该mongo实例上其他所有的操作。
那么如果是主从,在从上复制呢?这样意义也不大,从上数据无法恢复到主,复制出来也没有意义。

cloneCollection()

这个不多说,是远程复制。不是在本库中复制

mongoexport

mongoexport -d db_name -c src_collection | mongoimport -d db_name -c dst_collection --drop

这个说是最快的方法,个人感觉是最方便,快到不一定。
方便的原因就是一句话搞定。

mongodump

这个就两条了:

mongodump -d db_name -c src_collection
mongorestore --drop -d db_name -c dst_collection ./dump/db_name/src_collection.bson

需要生成一个文件。
但导出来说,mongoexport用的没有mongodump多。mongodump速度也非常快。
这是mongodump的优势
同时mongorestore就不那么给力,速度被质疑非常慢。(质疑的例子中,mongodb并没有停掉)

这个方法的优势是一切都在binary层面做的,不会有数据格式的转换。譬如find+insert方式,就会由不同的语言驱动导致数据类型变化,如int变成了double等。

总结

其实复制一个集合,这个本身需求就有点非常规。上面方法有诸多限制,可以根据自己的场景选择一个合适的,如果没有合适的,建议还是换一种实现方式。

mongodb我一直想把它作为内存缓存+传统的数据库的合体来用,又具备内存缓存的高速、同时数据操作锋利丰富、类似传统数据库、此外持久化也比较靠谱。相比之下,redis更擅长消息推送、天然排序-排行榜之类的事情。这些需要,mongodb做起来就比较吃力,需求一复杂,甚至会吃力到实现了但线上不可用。

新版战斗 重构记录 未完

资源的处理

技能脚本

  • 服务器
    所有的技能脚本都是放到服务器的本地目录中,一次性全部加载,反复使用。
  • 客户端
    每次需要异步加载技能脚本,有缓存使用缓存,全部加载完成之后才能做后续的数据处理。onExit后需要清除。

技能资源

所有技能所需图片全部都放入人物骨骼资源中。初始化人物骨骼的时候直接就加到缓存中了。

注意啦!!

*  每次进入游戏需要维护一个当前已经preload资源的列表,本次游戏中就不需要再次preload了

战斗场景

人物死亡后都做了些什么

  1. 攻击被命中,然后伤害处理,判断人物死亡。
  2. 从hl.battle.CM中移除该人物,如果是最后一个直接触发BattleOver。如果不是,进行下一步
  3. 触发CharacterDeath,则AI监听到。如果是自己那么进入死亡状态,如果是目标则重新寻找目标。
  4. 死亡状态下先执行SKill的onExit, 然后播放死亡动画,动画结束后执行Character的onExit
  5. Character中先移除Sprite的所有Action, 再移除所有关于本人物ID的所有自定义监听事件。
  6. 依次执行Body、AI的onExit函数,并且每个部分的onExit都包含hl.clear,这是用于清除身上所有字段。
  7. 最后这个人物就消失了。。。。

技能

技能处理的大概思路

我把技能总共分为四种:

  • 普通攻击
  • 依靠AttackCD(攻击间隙)触发,但是还需要满足一些其他要求:CD满7秒,攻击次数满10次等
  • 满足一些其他要求后立即触发
  • 大招

按照触发方式将他们分为三种: passive, active, manual,这三个都没有数据,直接使用普通攻击

当active中有满足条件的技能如果当前AI状态和Skill状态都允许,那么立即释放。
manual中的大招也是如此,只是优先级更高。

编写技能脚本需要注意些什么

  1. 必备属性:
    • type: "normal" || "passive" || "active" || "ult"
    • level: 1-4
    • init: 初始化函数
    • effect: 技能释放脚本
    • onExit: 技能中断处理函数
  2. 判断触发与否的事件监听都放入init中进行处理
  3. onExit中不但需要处理终端还要考虑资源回收
  4. 精灵的定位和动画所基于的位置分为两种,一种是插入到人物精灵身上;另一种是插入到层级上

技能攻击目标死亡

在effect开始执行的时候需要监听自己目标的情况,如果自己的所有目标都死了,那么技能需要中断。
一部分死掉了那么就处理剩下的一部分,一定要在人物伤害和效果之前去加上人物死亡的判断

大招的处理

大招需要考虑的相对多一点,在写effect中判断是否满足自己的触发条件。需要考虑下面的东西:
* 能量
* 是否为自动攻击
* 目标是否在范围内

沉默的处理

  1. 对目标触发SilenceStart 时间,修改其Skill中的_Silence属性
  2. 在Skill 播放技能的时候会先检测该技能是否为技能槽中的技能,如果是就return出去,

需要注意的点

  1. 战斗场景同意都用 1024 * 615
  2. 技能脚本有两份: 静态资源服务器 服务器生成战斗目录。 记得更新时要同步
  3. 战斗数据中无需带技能ID , 在服务器中技能脚本就以人物ID命名,每个人物对应着固定的技能

没有最好 只有更适合

在重构之前,我想了很多方案,看了很多相关资料。想尝试着把战斗部分的代码重构到一个自己满意的程度。尝试着使用ECS+FSM(实体 组件 系统和 有限状态机)的配合来达到代码的功能组件化以及数据驱动模式。个人感觉这种开发模式在端游中会很爽。这种模式好是好,但是不适合我们现在的项目情况。每次轮询所需要大量的计算成本和实体之间的通信以及组件之间的数据共享都不太适合我现在用,并且现在的游戏形式(人物功能类型比较少)体现不出什么这个架构的优势。也许是我还没有弄透彻吧!回过头来看,相比之下以前的方式和cocos的配合更好些,更适合现在这个游戏。浪费了这么长的时间,项目进度又被我拖慢了。已经整理好了重构目标和新的功能点,开始吧。

战斗场景文档

一些约定

  1. 为了保证能够全盘暂停,前端中需要把所有和人物角色有关系的计时器都挂载到Sprite上,这样就能够保证暂停的过程中可以全局暂停所有功能。

服务器端实现

1. 服务器端实现方案

  • Timer

  • actionManager

  • Action

  • cc.Node

    整体实现策略:Timer模拟实现cocos的update, 从而让actionManager能够和前端一样正常的跑起来。基于cc.Node的对象就能够正常的run moveTo、moveBy、Squence等Action。

2. 服务器端动画记录方案

目前是直接记录cc.Node层面的接口,__instanceId来作为node的标识

3. 前端播放方案

每个node的tag都是采用上面记录的__instanceId来作为标识

主要想说一下getNodeByTag,逐层去查找节点。。。非常悲催的实现方式

战斗资源

前端战斗

使用场景: 副本战斗

资源种类:
1. 每个战斗场景都需要的静态资源
2. 人物骨骼
3. 人物技能
4. 物品以及物品的品阶
5. 人物头像以及品阶

1 每个战斗场景都需要的:

  • 背景
  • 胜利界面()
  • 失败界面()
  • 战斗界面()

2 人物骨骼 (目录结构可以保证基本一致)

  • spine (png、atlas、json)
  • cocos (png、plist、json)
  • 人物ID和骨骼名称的映射关系(如果以ID命名就不需要了)

3 人物技能 (技能特效可以尽量保持目录结构的一致性)

  • 技能特效
  • 技能脚本

服务器端生成战斗

使用场景: PVP等

资源种类:
1. 人物骨骼
2. 人物技能
3. 物品以及物品的品阶
4. 人物头像以及品阶

前端播放

使用场景: PVP等

资源种类:
1. 每个战斗场景都需要的静态资源

其余资源都是服务器传过来

mongodb的条件count勿用

单纯的全表count()速度较快,可以接受。

但带条件查询的count()就慢许多。而且查到的结果数越多,就越慢的夸张。

数据表数据多的情况下,如用户表,千万不要用条件查询+count去计数。

-webkit-animation-fill-mode: forwards;导致无法滚动

条件:

  1. ios7.1*(其他平台未测)
  2. mobile safari
  3. 标签设置了-webkit-animation-fill-mode: forwards;

会导致overflow:scroll;-webkit-overflow-scrolling:touch;会无效。
还会导致后代标签的fixed失效。

建议:

  1. 过场动画尽量用transition去做。
  2. animation尽量只用在重复动画上,而尽量避免用在过场动画上。

mongoose findOneAndUpdate返回查找中的数组某项 失败

有一个集合,文档中有个字段是数组。需要根据id查询中某个数组,进行修改后同时返回出来。代码如下:

    model.findOneAndUpdate({
        arr: {
            $elemMatch: {
                id: item_id
            }
        }
    }, {
        $set: {
            "arr.$.xx" : true
        }
    }, {
        new: false,
        select: "arr.$"
    });

出来的结果是:

{
    arr: [
        {}, {}, {}, .....
    ]
}

所有的数组项全被选中,且返回的都是空对象。

只好改用了另外一个方法:

model.findOneAndUpdate({
        arr: {
            $elemMatch: {
                id: item_id
            }
        }
    }, {
        $set: {
            "arr.$.xx" : true
        }
    }, {
        new: false,
        select: $elemMatch: {
            id: item_id
        }
    });

返回:

{
    arr: [{
        id:xx,
        ....
    }]
}

mongoose的一些约定

1.访问、设置document的实例属性时,一定要用get和set方法。

原因:当mongoose的schema设置为strict为false时,即非严格模式,此时读取实例属性会得到undefined,必须通过get才行。示例:

// schema
var schema = new Schema({ name : { type: String } } , { strict : false});
var model = new db.model("test", schema);

// get a document
model.findOne({_id: 1}, function(err, doc){
    doc.name; // 得到是undefined
    doc.get("name"); // 得到正确地值
});

猜测是因为mongoose用到了defineProperty。例子中doc只是一个代理,当我们doc.name时,实际上是访问的不是doc,而是触发了doc的访问器,由访问器去读取隐藏在某处的数据,返回出来。这样做的好处是:数据和代理分开,代理上可以挂一些便捷方法,而不会污染数据。当然需要访问干净的数据时,譬如要JSON.stringify时,就需要通过doc.toObject()来拿到干净的数据,以便JSON序列化出来的数据是没有多余的。
当默认处于严格模式时,每个doc上的属性严格遵循schema,如果schema上没有声明某个属性,即便doc上存在,在保存doc时,这个属性也会被剔除掉。因为非常严格,mongoose可以从schema上分析得出每个doc上应该有哪些属性,然后用defineProperty进行监控。而我们取消严格模式时,doc身上的属性不再被mongoose强行剔除,因此它也不能确保分析schema得出的属性是完整的。在此情况下,会出现schema中没声明某个属性,用doc.xx的写法访问不到;schema中声明了某个属性,用doc.xx的写法就能访问到的情况。

鉴于有些情况下,必须采用mongodb的弱结构而把严格模式取消,因此得出结论:
在访问属性、设置属性时,一定要用get/set方法,来避免一些非常非常难以调试出来的bug。

关于mongodb读写分离的思考

一直有一个疑问:mongodb的副本集做读写分离,许多人都说能提高性能。如果从库恢复oplog时也会有写锁,那也就意味着主库有多少写压力,从库也就有多少写压力。之后,从库的写锁也会阻塞读。那么所谓的读写分离其实并没有提高性能。

我的疑问类似于:http://baike.1688.com/doc/view-d36934392.html

不过答案并没有解决疑问。

今天查到了一些资料:
http://stackoverflow.com/questions/18670191/mongodb-write-lock-blocks-reads-on-secondaries

其中答案提到:

从不会受到主写锁的影响。你可以通过mongotop 或者 mongostat查看写锁状态。
从会在主写锁后,在恢复oplog时,进行写锁。

确定了从库在恢复oplog时是会进行写锁的。
但仍然没解决主从写压力一样的问题。

之后又翻到这篇:http://grokbase.com/t/gg/mongodb-user/1283sxsmnt/is-write-lock-required-on-slave-node-when-applying-replication-from-master
提到:

从节点让读的权限比写锁优先级高(注:主节点应该是写贪婪的),他建议当从节点的读太高从而影响了oplog的恢复时,改用分片方案。

那么基本上明确了:

  1. 主从的写压力基本一样
  2. 从优先读,而且读太多会影响写

那么什么时候时候做读写分离?
从以上得出结论的话:
读写比例要大,即读多写少。如果写多读少,从库也会有大量写锁,阻塞读。
读多写少,从库虽然有写锁,但由于优先读的原因,读不受写锁阻塞,读的速度会加快。
不过从库读压力大的时候,只是暂时把写滞后,如果写滞后的很多,超过了主的oplog上限,会触发整体同步,造成主库压力过大,产生雪崩。
在从能轻松顶住读压力的时候,且读写比例是读多写少,可以考虑读写分离,提高读的速度
若从节点不能顶住读压力,最好放弃读写分离,换用分片,将热数据分散到不同的机器上。

关于备份mongodb实时快照

譬如每天10点,想把10点整这瞬间的快照备份下来。如果使用mongodump,不会对表造成锁,同时在dump期间,表数据仍然可以被修改。导致备份出来的数据,并不是10点这一瞬间的快照。
mongoexport据说也不会锁库。而且mongoexport导出的格式会有数据格式兼容问题。

  • mongodump时貌似有一个oplog的选项,回头看看。个人猜想:貌似可以通过oplog的设置,限制从节点备份时,忽略设置时间点以后的oplog。需要确定一下。
  • 至于恢复,mongorestore被说有速度特慢的问题。应该有办法解决。

配合说明

  1. 按周完成任务
  2. 每日在任务表中更新自己的进度
  3. SVN一般情况下,至少保持每1-2天提交一次代码
  4. SVN每次提交要写日志,尽量避免无用的提交
  5. 多总结、多记录、多分享

马力全开

kkserver

kkserver-core采用express+co+mongoose作为基础设施。

demo

设置

  1. route object 路由对象
  2. port number 服务器监听的端口,默认80
  3. api_root: string api逻辑的根目录,默认为process.cwd()
  4. has_static boolean 是否提供静态服务,默认false
  5. static_path 静态文件目录,默认为process.cwd() + "./public"
  6. db object 数据库设置:host,port,uname,pwd,dbname
  7. errcode_path string 错误码文件路径,必须指定。该文件为一个module
  8. model_path string mongoose的schema描述文件们的父目录。必填

路由规则

映射
服务器会把请求的path映射到api_root对应的目录中,如请求http://host/api/user/add这个网址,服务器
会执行api_root目录中的/api/user/add.js文件。

如果服务器不存在该文件,则返回404状态码。

接口文件约定
api_root中的JS文件,每个都是一个模块。模块必须导出一个函数,在请求来临时,会执行这个函数。为了方便阐述,模块导出的函数我们后面统一叫做接口处理函数。

  1. 模块导出函数(接口处理函数)必须是generator函数,譬如/api/user/add.js文件:
module.exports  = function * (req, res){
    // kachakacha...
}
  1. 接口处理函数中不能依赖回调,所有的回调必须用generator封装为co支持的规格。后面详述。
  2. 接口处理函数被执行时,会传入两个参数:req, res。这两个是express原生的req和res.
  3. kkcore在req, res其上添加了几个方法。都是以$开头,用来和原生方法做区分。
  4. 为了给前端返回数据方便且统一,res上封装了res.$okres.$err方法。返回数据强制使用这两个,不推荐使用express中的方法。

res上封装的方法列表

res.$ok
用法:

res.$ok();    // 前端得到:{"ok":1}
res.$ok({
    text: "123"
});               // 前端得到:{"ok":1,"text":"123"}

res.$err
$err接受errcode错误码,然后会从指定的错误码映射文件中,找到描述,返回前端。
假设config中设置errcode_path为"errcode.js",内容如下:

module.exports = {
    "1" : "手机号错误",
    "2" : "昵称长度错误",
    "3" : "密码长度错误",
    "4" : "手机号重复",
    "5" : "其他错误"
}

之后我们在接口处理文件中,做如下处理。

res.$err({
    errcode: 5
});
// 前端得到:{"ok":0,"errcode":5,"message":"其他错误"}。

综上,我们在做错误返回的时候,需要在errcode_path文件中加上错误码和对应的描述。然后在接口处理函数中返回时,只需要指定错误码即可。

错误码约定

  1. 接口处理函数如果有未捕获的错误,core会察觉到,并给前端返回错误码500.
res.$err({
    errcode: 500
});

全局变量污染

为了方便开发,设置了如下全局变量:co,DB,isError,$type,log

DB

DB提供了在mongoose的基础上,进一步进行了约定。
在config中指定了model_path,在这个目录中,存在各个模块文件,这些模块描述了数据库中不同表的结构。
1.一个模块代表一个表。
2. 每个模块文件的文件名即表名(也就是mongodb中的集合名)
3. 标的结构是通过mongoose中schema的概念来描述的

举个例子,下面是一个model_path指定的目录:

|-models
|----user.js
|----arc.js

user.js的代码如下:

module.exports = DB.defineModel("user", {
    name  : {                       // 登陆账号
        type: String,
        required: true,
        unique: true
    },
    headimg : {                 // 头像路径
        type: String,
        required: true
    }
});

arc.js代码如下:

module.exports = DB.defineModel("arc", {
    title  : {                       // 文章标题
        type: String
    },
    text : {                    // 正文
        type: String
    }
});

这里可以看到,数据model的模块代码,只是执行了一个函数,并把这个函数执行结果赋值给module.exports。

DB.defineModel
defineModel会生成一个包装过的mongoose的model实例。提供一些mongoose的model的方法的yield版,均已$开头。目前有$find、$findOne等。这里的设计目前还不够好。将来改正或取消。
有两个参数,第一个是表名(集合名),硬性要求必须和模块文件名一样。譬如user.js里第一个参数就必须是'user'。第二个参数是数据库结构的描述,用于实例化mongoose中的schema。所以可以参考mongoose的文档说明。
第二个参数我们假设为schemaDesc,最终core中执行的代码类似如下:

var schema = new mongoose.Schema(schemaDesc);

DB.M()
DB.M("modelname")获取指定名字的model,也就是一个扩展Model实例。这里的model都是通过defineModel包装过的。如果要访问原生的mongoose model,可以DB.M("modelname").model即可。

扩展Model的方法列表

  1. model.create([data])
    参数:data,可选,作为文档原始数据
    创建并返回一个document,完全等价于mongoose中的document。
var model = DB.M("user");
// 创建document的两个办法
var doc = model.create();
doc.name = "xiaoming";
doc.password = "111111";

// 下面创建一样
var doc = model.create({
    name: "xiaoming",
    password: "111111"
});
  1. model.$save(document)
    参数:document,必填,model.create()创建的document
    保存指定的文档。这是一个generator版的函数,用于配合yield使用。如果特殊情况下需要回调(譬如外部无法用generator函数包裹),可以直接用mongoose中的document.save(callback)写法。避免这种回调写法
// 两种保存方式
var doc = model.create({name:"123");
// 1.yield的同步书写方式
module.exports = function *(){
    yield doc.$save();
};
// 2. 不推荐的方式
doc.save(function(err){
    if (err){}
});
  1. 基于mongoose的API而包装的方法
    这些方法本质上都是执行mongoose的API,只是把这些回调形式的API改为了generator函数,以便yield控制。
    目前已包装的方法有:"find", "findOne", "remove", "findByIdAndUpdate", "insert", "update", "findOneAndUpdate".
    包装前的API具体使用文档参照:http://mongoosejs.com/docs/api.html#model-js内model部分的对应的API。
    注意:包装后的方法名前面会多出一个$,即find会变成$find.
    以find为例:
// 1. mongoose原生的写法
model.find({}, function(err, docs){
    if (err){
       // ...
    }
    // 拿到结果docs,进行处理
});

// 包装后的写法
module.exports = function * () {
    var docs = yield model.$find({});    // 注意这里find需写成$find
    if (isError(docs)){    // 如果返回的是个错误
        return;
    };

    // 拿到正确的结果docs,进行处理。
}

注意:mongoose中的方法findByIdAndUpdate本质是调用了findAndModify函数,findAndModify和update的区别在于:
update是只更新,但不返回更新后的文档。
findAndModify在更新后,还会返回更新后的文档。实际上findAndModify应该叫做updateAndReturn才对。
需要用findAndModify时,请用mongoose特有的findByIdAndUpdatefindOneAndUpdate代替。

isError

isError函数很简单,用来判断一个对象是否是Error构造函数的实例。用法:isError(err)
由于引入co,所有的接口处理函数都要已yield形式去处理异步任务,不能再使用回调(大部分情况)。在kkcore中错误处理更像go语言,在一个异步任务结束后,经常(接近100%)会判断返回的结果是否是一个错误。为了方便书写,提取了这么一个简单的函数作为全局函数。

$type

用来判断某变量的数据类型。目前没有太大用处,可能会取消。

log对象

log对象上提供了5个方法,方便做日志记录。目前是采用console进行输出,将来会换成更适合生成环境,把日志正规化。
5个方法:"debug", "log", "info", "warn", "error"

co

会取消。

express的中间件

数据解析采用了bodyParse,也就是说客户端发送数据只能用GET或POST&"application/x-www-form-urlencoded"。客户端发送Formdata格式的数据,暂不支持。也不支持上传文件。

注意:

为了方便开发,会污染全局变量:co,DB,isError,$type,log

cocos、egret测试

测试一

一、测试环境

  1. 微信 6.1
  2. cocos2d-js 3.1
  3. iphone 5s
  4. ios 8.1.2

二、测试内容和目的

内容: 静态图片不停的moveTo
目的: cocos2d-js在canvas和webgl模式下的表现

三、测试结果

两种模式下的数值:
225 pic_hd

226 pic

228 pic

两种模式下的曲线对比:
229 pic

四、测试样例

canvas: http://www.zhkuang.com/cocos/html5-canvas/
webgl: http://www.zhkuang.com/cocos/html5/

测试二

一、测试环境

  1. 微信 6.1
  2. egret
  3. iphone 5s
  4. ios 8.1.2

二、测试内容和目的

内容: 静态图片不停的移动(白鹭使用的是官方测试用例)
目的: egret 在微信和safari中 不同渲染模式之间的差异

三、测试结果

webgl 和 canvas:
247 pic_hd

四、测试样例

egret:http://docs.egret-labs.org/benchmark/bitmap/

测试三

一、测试环境

  1. 微信 6.1
  2. cocos2d-js 3.3
  3. iphone 5s
  4. ios 8.1.2

二、测试内容和目的

内容: 静态图片不停的moveTo
目的: cocos2d-js 3.3版本 在canvas和webgl模式下的表现

三、测试结果

两种模式下的数值:
255 pic

四、测试样例

canvas: http://www.zhkuang.com/cocos/cocos2d-js-v3.3/2-28-canvas/
webgl: http://www.zhkuang.com/cocos/cocos2d-js-v3.3/2-28-webgl/

mongodb内嵌数组的使用

内嵌数组由于操作符和性能的限制,导致使用起来需要小心一点。

有一个场景:数据这么组织:

{
    _id: "xxx",
    items: [
        {
            id: 1,
            val: 3
        },
        .....
    ]
}

当想实现,存在id为1则更新,不存在则插入时,惯用的$addToSet不再好使,因为其是进行对象全匹配,需要id和val都一样才认为存在。
此时要实现一次业务上的更新非常恶心,需要两条读写操作才能搞定。且无法保证原子性。
目前想到的办法是改下数据结构:

{
    _id: "xxx",
    items: [ 1, 2, 3 ....],
    id_1: 3,
    id_2: 4,
    ....
}

这种情况下可以使用$addToSet做插入,配合$set直接设置id_{id}值。解决更新时的问题。

接下来是查询操作。查询假设不分页、不排序,单纯的列举所有item的val值。那么此时就不如第一个数据结构好,虽然都是一次查询,但第二个数据结构需要在语言层把id和val拼起来,消耗服务器性能。

此外,如果排序,则显得有些麻烦。如果排序+返回固定个数,就更糟糕了。

内嵌数组的形式,实现查询较为简单直观,但要做有条件的插入文档,会很恶心。
数组不大时,排序一下内部查询一下,消耗也是可以接受。
但如果数组较大,还涉及到排序、数组内文档查找,会非常消耗CPU,建议更换数据结构。

第二种数据结构,便于实现:存在更新、不存在插入、类似upsert的操作,且是原子性的。但查询只能实现简单的全查询,有条件的查询就比较麻烦。适合这个数组经常更新,但查询很少很少的这种场景。不过也不尽然,只要查询能实现且性能能满足,为了保证数据原子性,即便这个数组不经常更新,也会采用这种方式,此时酌情在客户端做上缓存,也是很不错的。不过后期数据的查询扩展性会受到影响。
切记,一旦查询条件不简单,譬如要排序、分页,数据就需要全加载到语言层中进行排序分页,要保证你能接受这个消耗。

行内标签无法使用transform

一、问题出现环境:

  1. modile端webkit内核浏览器;
  2. PC端老式webkit内核浏览器(最新的支持);

二、解决办法:
将行内元素设置 display:block;

kkserver-全局事件

user_hero_count_update 英雄数量更新

参数名
uid 玩家id
new_count 新的数量
increase_num 增长的数量

建立错误收集机制

项目的前端部分,尤其是单页webapp,因为其复杂度更高,更容易出现不易发现、漏掉的错误。因此一个健全的前端,也需要和后端一样有自定义错误、错误收集、追踪、分析系统。

利用error事件

error事件有三个参数:message、url、line,分别为错误消息、JS文件路径、出错行数。通常来说,这三项足够分析定位到出错文件,帮助分析到原因;但在现在流行的上线合并中,很难定位到具体是哪个文件。

示例:

window.onerror = function(msg, url, line){
     // 把错误相关信息发送给服务器
}

只有error事件监听是不够的。我们还得考虑try catch。比如下面的代码,就不会触发error事件:

try {
    var data = JSON.parse('xx');
}catch(err){
    alert('解析错误');
    return;
}

// 处理data的其他逻辑代码

上面代码的本意是希望出错告知用户,且不在继续下面的代码。但无意中阻止了错误的收集。

要解决这个问题,那么至少要约定catch中一定要抛出错误,以便收集到每个错误。除非这个错误是开发者十分确定、不需要被收集的。

上面的代码应改为:

try {
    var data = JSON.parse('xx');
}catch(err){
    alert('解析错误');
    throw err;
}

// 处理data的其他逻辑代码

并发下的数据一致性

我并不知道mongodb的驱动是否是按照顺序执行请求的。因为网络的复杂性,可能导致数据的覆盖。

举一个具体例子:
有玩家获得了分数90,之后又立马获得了分数100(假设这两者之间的间隔接近0ms)
那么客户端会这么执行

  1. 先发送更新玩家分数为90分的请求,给mongodb,等待回应
  2. 之后立马发送更新100分的请求给mongodb,等待回应。
  3. 由于客户端使用了连接池,两个请求从两个连接发出,由于网络的原因,可能100分的请求先到达mongodb
  4. mongodb执行100分的更新,回应
  5. mongodb执行90分的更新,回应

最后玩家的分数成为了90分。

以上发送的情况很小,但也不是没有可能。当客户端存在某种场景,使得两次同性质的更新同时执行,就有可能出现这种覆盖的情况。

mongodb自身是有操作符搞定了原子性,如$inc,$set等。但是连接来的先后顺序是否影响执行顺序,我并没有翻阅到相关文档。在此存疑的基础上,上面的情况就有可能发生。

一般情况下,$inc的操作是不会出现覆盖的,因为它是基于之前的值进行增减,无论先先加5后加10,还是反过来,最终都是加了15.没有影响。不过需要注意的是有最大值限制时,就会有问题。比如最大值是12,当前是0.如果玩家先加10后加5,但是执行的顺序是先5后10,最终玩家分数本应该是10,缺变成了5.

影响较大的是$set,由于$set是直接覆盖,不急于原先的值,先后不一致肯定导致覆盖。

如果非要解决方案的话,数据库方面我觉得是非常看使用场景,综合产品逻辑、客户端、服务器等各方面综合考量而得出一个“最适合的方案”。

由于问题出在连接到达顺序和发出顺序不一致导致的,那么头疼医头,保证连接到达顺序和发出顺序一致就可以了。
譬如:做一个单点服务器,专门处理这些会同时发出请求的业务。业务服务器和这个单点服务器保持单个长连接而非多个,设计一个协议,保证先发一定先到。
当然前提是前端到业务服务器也是先发先到的~~

讨论问题记录

1. 缓存策略

(有待确认)每次进入游戏之前都需要请求最新的版本号,后面所有的请求全部都带有最新的版本号。由于加长http的过期时间和采用manifest没什么大的性能差异,所以排除manifest策略。

2. 热更新

3. 资源服务器的架构和实现方式

4. 技能动画资源

有些技能动画只需一张图,有些需要帧动画,存放位置和资源结构需要确定一下。

搭建SVN服务器

环境

centos 6.3 64位

安装yum install -y subversion
验证版本svnserve —version

svnserve,版本 1.6.11 (r934486)
   编译于 Mar  6 2014,10:49:10
...

创建仓库:svnadmin create /data/svn/001

pwd /data/svn/001/conf

cd /data/svn/001/conf

看到三个文件
svnserve.conf: svn服务配置文件下。
passwd: 用户名口令文件。
authz: 权限配置文件。

svnserve.conf 文件, 该文件配置项分为以下5项:

  • anon-access: 控制非鉴权用户访问版本库的权限。
  • auth-access: 控制鉴权用户访问版本库的权限。
  • password-db: 指定用户名口令文件名。
  • authz-db:指定权限配置文件名,通过该文件可以实现以路径为基础的访问控制。
  • realm:指定版本库的认证域,即在登录时提示的认证域名称。若两个版本库的认证域相同,建议使用相同的用户名口令数据文件
    配置完这上述三个文件

启动SVN服务svnserve -d -r /data/svn/001
-d 表示后台运行
-r 指定根目录是/data/svn/001

查看是否启动: ps -ef | grep svn

停止直接kill -9

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.