【问题标题】:Does PhantomJS share memory between multiple requests?PhantomJS 是否在多个请求之间共享内存?
【发布时间】:2015-03-03 00:57:58
【问题描述】:

我正在使用PhantomJS + d3 将美国邮政编码的地图呈现为后端进程。渲染和邮政编码计算需要很长时间,以至于将 html 和 d3 js 放入浏览器需要一分钟才能加载并导致其他问题,因此我们将其移至后端。

如果我通过 curl 向 PhantomJS 启动的节点服务器发送一个请求,没问题。如果我将我们的多个地图请求间隔大约 15 秒,也没有问题。但是,如果我很快启动几个 curl 请求,渲染的图像最终看起来是一样的(也就是将相同的图像写入多个文件。)这是幻像脚本:

var 端口, 服务器, 服务, 页, 网址, svgDrawer;

fs     = require('fs');
port   = 9494;
server = require('webserver').create();
page   = require('webpage').create();

service = server.listen(port, function (request, response) {
    var drawerPayload = null;
    try{
        drawerPayload=JSON.parse(request.post);
    } catch(e){
        response.statusCode = 417;
        response.write("Error : Invalid Input JSON");
        response.close();
        return;
    }

    url = 'file:///' + fs.absolute('./'+drawerPayload.inFile);
    page.open(url, function (status) {
        if(status=="success"){
            page.evaluate(function(data){
                $("body").on( "click", data, chartBuilder );
                $("body").click();

                var maxtimeOutMillis = 15000,
                    start = new Date().getTime(),
                    condition = false,
                    interval = setInterval(function() {
                        if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
                            condition = $("svg.chart").hasClass("done"); //< defensive code
                        } else {
                            if(!condition) {
                                clearInterval(interval)
                            } else {
                                page.render(drawerPayload.outFile);
                                clearInterval(interval); //< Stop this interval
                            }
                        }
                    }, 250); //< repeat check every 250ms
            });

            response.statusCode = 200;
        } else {
            response.statusCode = 404;
            response.write("Not Found"+url);
        }
        response.close();
        return;
    });

    page.onError = function (msg, trace) {
        console.log(msg);
        trace.forEach(function(item) {
            console.log('  ', item.file, ':', item.line);
        })
        response.statusCode = 417;
        response.write("Error : "+msg);
        response.close();
        return;
    }
});

html+d3 看起来像这样:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.zip {
    stroke: none;
}
.chart {
    fill: white;
    width: 1000px;
    height: 500px;
}
</style>
<body>
    <div id="chart-container">
        <svg class="chart"></svg>
    </div>
</body>

<script src="./jquery-min.js"></script>
<script src="./d3.min.js"></script>
<script type="text/javascript" src="http://d3js.org/topojson.v1.min.js"></script>
<script>

function chartBuilder(e){
    var zip_data = e.data;
    $.getJSON("zips_us_topo.json", function(us){
        console.log("rendering...\n");
        var width = 1000;
        var height = 500;
        var projection = d3.geo.albersUsa()
            .scale(width)
            .translate([width / 2, height / 2]);

        var path = d3.geo.path().projection(projection);
        var color = d3.scale.log().domain([1,zip_data.max+1]).range(["#cccccc","#f63337"]);

        var svg = d3.select("svg.chart")
            .attr("width", width)
            .attr("height", height)
            .style({margin: "10px 100px"})
            .append("g")
            .attr("class", "counties")
            .selectAll("path")
            .data(topojson.feature(us, us.objects.zip_codes_for_the_usa).features)
            .enter()
                .append("path")
                    .attr("class", "zip")
                    .style({fill: function(d){
                        return color(zip_data.counts[d.properties.zip] ? zip_data.counts[d.properties.zip]+1 : 1);
                    }})
                    .attr("d", path);
        svg.classed("done", true);
    });
}
</script>

如果请求都同时卷曲,则看起来它正在将一张图像写入所有输出文件。 PhantomJS 是为每个请求创建一个新页面,还是每次都加载相同的请求?

【问题讨论】:

    标签: d3.js phantomjs


    【解决方案1】:

    对于所有到来的请求,您只有一个 page 实例。当新请求进入并劫持当前的page.open() 请求时,这可能会产生一些竞争条件。基本上有两种方法可以解决这个问题,具体取决于您的首选方案。

    多个“标签”

    简单的解决方法是为每个请求创建一个新的page 实例,它们将是同一浏览器中本质上不同的选项卡。因此,如果 cookie 或 localStorage 是一个问题,那么这不适合您。

    page = require('webpage').create(); 移动到server.listen 回调中,使用后不要忘记close() page 实例。

    一次只有一个请求

    由于这是一个运行时间不短的进程,您可以在 page.open() 当前未运行时启动它,并将所有传入请求放入队列中,只要 page.open() 尚未完成。完成后,保存响应,通过请求队列并以相同的响应响应所有的响应。

    如果确实有很多并发请求,这当然比第一个解决方案在内存消耗上要好得多。


    您的代码还有其他问题。您在page.evaluate() 中使用setInterval(),它会中断控制流。 response.statusCode = 200; 很可能会在页面呈现之前设置。

    page.evaluate() 内部的 page.render() 是另一个问题。 page.evaluate() 是沙盒页面上下文。它无法访问在其外部定义的变量,包括pagerequire。 (Solution 这个孤立的问题)

    这两个问题可以通过在页面上下文之外等待它内部的渲染条件来解决。我建议使用waitFor from the examples:

    if(status=="success"){
        page.evaluate(function(data){
            window._finishIndicationVariable = false;
            $("body").on( "click", data, chartBuilder );
            $("body").click();
    
            var maxtimeOutMillis = 15000,
                start = new Date().getTime(),
                condition = false,
                interval = setInterval(function() {
                    if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
                        condition = $("svg.chart").hasClass("done"); //< defensive code
                    } else {
                        if(!condition) {
                            clearInterval(interval)
                        } else {
                            window._finishIndicationVariable = true;
                            clearInterval(interval); //< Stop this interval
                        }
                    }
                }, 250); //< repeat check every 250ms
        });
        waitFor(function check(){
            return page.evaluate(function(){
                return window._finishIndicationVariable;
            });
        }, function onReady(){
            page.render(drawerPayload.outFile);
            response.statusCode = 200;
            response.close();
        });
    } else {
        response.statusCode = 404;
        response.write("Not Found"+url);
        response.close();
    }
    

    注意response.close();被使用了两次,因为其中一个if分支是异步的,一个不是。

    【讨论】:

    • 谢谢,这看起来方向正确。但是它提出了一个问题——如果没有全局范围内的页面,我可以再在 page.evaluate 中调用 page.render。你能帮忙吗?
    • 我错过了那部分。无论在何处定义 page,都无法在 page.evaluate() 内部调用 page.render()。我只找到了一个 CasperJS 示例,但它是同一件事:A: Use casper function inside casper.evaluate()
    • 是的,我刚刚发现:phantomjs.org/api/webpage/method/evaluate.html。如何在呈现页面之前等待页面完成评估?我应该暂停一下吗?
    • 您已经在使用类似waitFor 的函数来等待页面上下文中的条件,因此您可以使用类似的函数直到内部函数完成。我会扩展我的答案。
    猜你喜欢
    • 2016-03-29
    • 2016-01-05
    • 2020-06-16
    • 2015-08-25
    • 2013-03-10
    • 2014-07-16
    • 2021-06-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多