最近开发一个机器人程序,其主要功能是模拟玩家在地图上跑动用来测试地图压力。主要功能代码如下:

loop(State) ->
	#state{robot_state = RobotState} = State,
	case RobotState of
		born ->
			NewState = robot_born(State),
			NextRobotState = get_next_robot_state(),
			erlang:send_after(random:uniform(3000), self(), loop),
			NewState#state{robot_state = NextRobotState};
		move ->
			robot_move(State),
			State;
		move_to_npc ->
			robot_move_to_npc(State),
			State;
		_ ->
			todo
	end.
robot_move_to_npc(State) ->
	#state{robot_player_id = RobotPlayerID, send_socket = _SendSocket} = State,
	{CurMapDataID, CX, CY} = get_cur_robot_pos(State),
	{TX, TY} = get_a_npc_point(CurMapDataID, CX, CY),
	PathList = aStar:find_path(CurMapDataID, CX div 100, CY div 100, TX, TY),
	case PathList of
		[] ->
			erlang:send_after(5000, self(), loop);
		PathList ->
			PosInfoList = [#pk_PosInfo{x = X * 100, y = Y * 100} || {X, Y} <- PathList],
			debug:rpc_call(RobotPlayerID, pp_move, on_C2GS_PlayerMoveTo, [undefined, #pk_C2GS_PlayerMoveTo{posX = CX, posY = CY, curMapDataId = CurMapDataID,
																										   posInfos = PosInfoList}]),
			erlang:put(move_target, {TX, TY}),
			erlang:send_after(1000, self(), {check_move_complete, 0})
	end.
<span ></span><pre name="code" >get_a_npc_point(MapDataID, CX, CY) ->
<span >	</span>NPCList = data_npc:get_items(),
<span >	</span>F = fun(#npcCfg{map = Map}) ->
<span >			</span>Map =:= MapDataID
<span >		</span>end,
<span >	</span>MapNPCList = lists:filter(F, NPCList),
<span >	</span>#npcCfg{posx = TX, posy = TY} = lists:nth(random:uniform(length(MapNPCList)), MapNPCList),
<span >	</span>{TargetX, TargetY} = mod_mirror:get_fix_point(CX div 100, CY div 100, TX, TY, 2, MapDataID),
<span >	</span>{TargetX div 100, TargetY div 100}.
 
<span >如上所示,一个机器人就是一个单独的进程,机器人进程start起来后每隔一段loop,loop主要做的事情就是寻找一个点,并且让机器人移动过去。很简单的一个功能,但是出现了问题。在调试过程中发现一个机器人进程占用内存资源太多,居然有40M之多。在虚拟机上测试,1G内存在几秒时间内消耗90%以上。在手动gc的情况下,内存占用回到16M,但是还是太多。</span>
 

怀疑自己写的代码有问题,祭出神器——逐层扫描调试。首先把loop的主要功能屏蔽,机器人进程不干任何事情,process_info(PID, memory)查看内存占用,意料之中得到{memory,2560}。

第二步,屏蔽掉A*寻路过程,留下寻找目标点的过程。部分函数修改如下

robot_move_to_npc(State) ->
	#state{robot_player_id = RobotPlayerID, send_socket = _SendSocket} = State,
	{CurMapDataID, CX, CY} = get_cur_robot_pos(State),
	{TX, TY} = get_a_npc_point(CurMapDataID, CX, CY),
	todo.
查看内存占用大概20M左右,说明在寻找目标点的函数get_a_npc_point/3有大量内存消耗。

第三步,进入get_a_npc_point/3,继续第二步方法,拆分每个语句,查看占用情况,修改如下

get_a_npc_point(MapDataID, _CX, _CY) ->
	NPCList = data_npc:get_items(),
	F = fun(#npcCfg{map = Map}) ->
				Map =:= MapDataID
		end,
	MapNPCList = lists:filter(F, NPCList),
	#npcCfg{posx = TX, posy = TY} = lists:nth(random:uniform(length(MapNPCList)), MapNPCList),
	{TX, TY}.
修改之后,内存占用情况大为好转,显然问题在mod_mirror:get_fix_point这个方法上。这个方法的目的是根据给定的一个点,随机返回其周围八个方向的一个可以行走的点。在判断随机出来的点是否可以行走时,会根据给定的MapID在地图数据ETS表中查找该地图中所有不可走的点来做判断。由于ets:lookup/2查找出来地图数据结构过大,EVM在执行完该ets:lookup/2之后,需要在进程中开辟出更多的空间来存储,所以进程占用的内存就变多了。但是这种机制是erlang本身的内存管理方式,程序猿并没办法改变。

结论:1、erlang每次执行的结果都会在进程里开辟空间来存储,如果执行结果赋值给了一个变量,那么该变量只是执行结果所在内存的一个指针。2、如果一个进程内存能通过手动gc明显降下来,说明很可能和你的代码无关,是执行结果太过大。3、如果在这种情况下需要减少进程占用的内存空间,可以在代码里面添加erlang:garbage_collect,前提是你的进程有足够的处理时间来做gc(在这个内存不值钱的年代不提议这样做,尤其是在对处理速度要求非常高的情况下)。

附:

1、初始占用内存

(gameserver_1@localhost)8> erlang:process_info(erlang:whereis(robot_10_1), memory).
{memory,33657600}

2、删除mod_mirror:get_fix_point这个方法后占用内存

(gameserver_1@localhost)16> erlang:process_info(erlang:whereis(robot_10_1), memory).
{memory,2543448}

3、不删除mod_mirror:get_fix_point这个方法,增加erlang:garbage_collect的内存占用

(gameserver_1@localhost)22> erlang:process_info(erlang:whereis(robot_10_54), memory). {memory,4714992}