php性能分析工具xhprof分析

php性能分析工具xhprof分析

facebook醋闭,做為世界上最大的php應(yīng)用網(wǎng)站,為php貢獻(xiàn)出了hhvm xhprof等優(yōu)秀開(kāi)源工具朝卒,其中xhprof已成為很多phper調(diào)試php性能瓶頸的利器证逻。本文作者將從xhprof源碼出發(fā),看看xhprof是怎么做到性能分析的

關(guān)鍵數(shù)據(jù)結(jié)構(gòu)

xhprof主要只使用了如下兩個(gè)數(shù)據(jù)結(jié)構(gòu):

xhprof的兩種分析模式

1扎运、XHPROF_MODE_HIERARCHICAL模式瑟曲,該模式是詳細(xì)分析整個(gè)PHP代碼的執(zhí)行情況饮戳,其輸出的分析數(shù)據(jù)如下:


array(7){["main()==>load::./inc.php"]=>array(5){……}["main()==>run_init::Test/inc.php"]=>array(5){……}["bar==>echoHello"]=>array(5){……}["foo==>bar"]=>array(5){……}["main()==>foo"]=>array(5){……}["main()==>xhprof_disable"]=>array(5){……}["main()"]=>array(5){["ct"]=>int(1)["wt"]=>int(390372)["cpu"]=>int(392000)["mu"]=>int(15040)["pmu"]=>int(10024)}}

2、XHPROF_MODE_SAMPLED模式洞拨,該模式每隔0.1秒取樣一次扯罐,記錄當(dāng)前執(zhí)行的堆棧,其輸出的分析數(shù)據(jù)如下:


array(5){["1460294938.300000"]=>string(30)

"main()==>foo==>bar==>echoHello"["1460294938.400000"]=>string(30)

"main()==>foo==>bar==>echoHello"["1460294938.500000"]=>string(30)

"main()==>foo==>bar==>echoHello"["1460294938.600000"]=>string(30)

"main()==>foo==>bar==>echoHello"["1460294938.700000"]=>string(30)

"main()==>foo==>bar==>echoHello"

}

該模式通過(guò)使用xhprof-flamegraphsFlameGraph可生成flame graph烦衣,如下圖(我的測(cè)試代碼的圖太簡(jiǎn)陋歹河,就用xhprof-flamegraphs的圖代之了= =):

XHPROF_MODE_HIERARCHICAL模式分析

一、xhprof_enable([ int $flags = 0 [, array $options ]] )的第二個(gè)參數(shù)$options用于過(guò)濾掉不想被profile的函數(shù)花吟,過(guò)濾函數(shù)功能的實(shí)現(xiàn):

1秸歧、在xhprof_enable()中會(huì)先執(zhí)行:hp_get_ignored_functions_from_arg(optional_array),將要忽略的函數(shù)存儲(chǔ)到char **hp_globals.ignored_function_names中衅澈。

2键菱、接著執(zhí)行hp_ignored_functions_filter_init()初始化uint8 hp_globals.ignored_function_filter[XHPROF_IGNORED_FUNCTION_FILTER_SIZE],具體代碼如下:

static

void

hp_ignored_functions_filter_init(){

if(hp_globals.ignored_function_names

!=NULL){inti=0;for(;

hp_globals.ignored_function_names[i]

!=NULL;

i++){char*str=

hp_globals.ignored_function_names[i];uint8hash=

hp_inline_hash(str);

//根據(jù)函數(shù)名做hashhash >> 3

intidx=INDEX_2_BYTE(hash);

hp_globals.ignored_function_filter[idx]

|=INDEX_2_BIT(hash);

//1 << (hash & 0x7)

}}

}

因?yàn)閄HPROF_IGNORED_FUNCTION_FILTER_SIZE為 32今布,所以INDEX_2_BYTE(hash)將hash右移3位经备,高位補(bǔ)0,確保得到的idx不會(huì)超過(guò)32部默。

hp_globals.ignored_function_filter是uint8類型數(shù)組侵蒙,所以INDEX_2_BIT(hash)就是將hash映射到這8個(gè)bit中的某個(gè)位置。

也就是說(shuō)一個(gè)hp_globals.ignored_function_filter的元素有可能保存多個(gè)hash值的映射傅蹂。

3纷闺、過(guò)濾的判斷是通過(guò)hp_ignore_entry()->hp_ignore_entry_work()進(jìn)行的,具體代碼:

int

hp_ignored_functions_filter_collision

(uint8hash){uint8mask=INDEX_2_BIT(hash);return

hp_globals.ignored_function_filter

[INDEX_2_BYTE(hash)]&mask;}

/*該方法首先判斷curr_func的hash是否在過(guò)濾列表

hp_globals.ignored_function_filter中如果存在份蝴,因?yàn)榇嬖趆ash碰撞犁功,

那么還需要判斷curr_func是否

在hp_globals.ignored_function_names中hp_globals.ignored_function_filter的存在就是

為了減少直接根據(jù)函數(shù)名去判斷是否需要過(guò)濾*/

int

hp_ignore_entry_work

(uint8hash_code,char*curr_func){intignore=0;if(

hp_ignored_functions_filter_collision

(hash_code)

){inti=0;for(;

hp_globals.ignored_function_names[i]

!=NULL;i++){char*name

=hp_globals.ignored_function_names[i];if(!strcmp(curr_func,name)){ignore++;break;}}}returnignore;

}

二、打點(diǎn)采集性能數(shù)據(jù)的實(shí)現(xiàn):

在hp_begin(long level, long xhprof_flags TSRMLS_DC)中搞乏,替換掉了zend內(nèi)核execute_data的執(zhí)行函數(shù)以及一些編譯代碼的函數(shù)波桩,相當(dāng)于加了一層proxy戒努,部分代碼如下:

_zend_compile_file=zend_compile_file;

//編譯PHP文件

zend_compile_file=hp_compile_file;_zend_compile_string=zend_compile_string;

//PHP的eval函數(shù)

zend_compile_string=hp_compile_string;_zend_execute_ex=zend_execute_ex;

//execute_data的執(zhí)行函數(shù)

zend_execute_ex=hp_execute_ex;_zend_execute_internal=zend_execute_internal;

//內(nèi)部函數(shù)(C函數(shù))的執(zhí)行

zend_execute_internal=hp_execute_internal;

在每一層proxy中请敦,都會(huì)調(diào)用BEGIN_PROFILING和END_PROFILING,以hp_execute_ex為例:

ZEND_DLEXPORT

void

hp_execute_ex

(zend_execute_data*execute_dataTSRMLS_DC){……BEGIN_PROFILING(

&hp_globals.entries,func,

hp_profile_flag);

//函數(shù)執(zhí)行前打點(diǎn)

#if PHP_VERSION_ID < 50500

_zend_execute(opsTSRMLS_CC);

#else

_zend_execute_ex(execute_dataTSRMLS_CC);

#endif

if(hp_globals.entries){END_PROFILING(&hp_globals.entries

,hp_profile_flag);

//函數(shù)執(zhí)行結(jié)束記錄統(tǒng)計(jì)信息

}

efree(func);

}

三储玫、xhprof_disable輸出數(shù)據(jù)中ctwt的實(shí)現(xiàn)

ct是當(dāng)前代碼塊被執(zhí)行的次數(shù)侍筛,在END_PROFILING->hp_globals.mode_cb.end_fn_cb->hp_mode_hier_endfn_cb->hp_mode_shared_endfn_cb中:

hp_inc_count(counts,"ct",1TSRMLS_CC)

在每次代碼塊執(zhí)行結(jié)束后就會(huì)對(duì)其對(duì)應(yīng)的ct增1。

wt是當(dāng)前代碼塊總的執(zhí)行時(shí)間(wall clock time)撒穷,在END_PROFILING->hp_globals.mode_cb.end_fn_cb->hp_mode_hier_endfn_cb->hp_mode_shared_endfn_cb中:

tsc_end=cycle_timer();hp_inc_count(

counts,

"wt",

get_us_from_tsc(tsc_end-top->tsc_start,hp_globals.cpu_frequencies

[hp_globals.cur_cpu_id])

TSRMLS_CC);

top->tsc_start是在BEGIN_PROFILING->hp_globals.mode_cb.begin_fn_cb->hp_mode_hier_beginfn_cb()中通過(guò)cycle_timer()獲得的匣椰,具體代碼:

//通過(guò)rdtsc匯編指令獲取CPU時(shí)鐘周期

staticinlineuint64cycle_timer(){uint32__a,__d;uint64val;asmvolatile("rdtsc":"=a"(__a),"=d"(__d));(val)=((uint64)__a)|(((uint64)__d)<<32);returnval;

}

hp_globals.cpu_frequencies[hp_globals.cur_cpu_id]存儲(chǔ)了各個(gè)CPU對(duì)應(yīng)的時(shí)鐘頻率,時(shí)鐘頻率的獲取是通過(guò)如下方式:

static

double

get_cpu_frequency(){structtimevalstart;structtimevalend;if(gettimeofday(&start,0)){perror("gettimeofday");return0.0;}uint64tsc_start=cycle_timer();/* Sleep for 5 miliseconds.

Comparaing with gettimeofday's

few microseconds* execution time, this should be enough. */usleep(5000);if(gettimeofday(&end,0)){perror("gettimeofday");return0.0;}uint64tsc_end=cycle_timer();// 時(shí)鐘周期數(shù)/時(shí)間 = 時(shí)鐘頻率

return

(tsc_end-tsc_start)*1.0

/(get_us_interval(&start,&end));

}

static

void

get_all_cpu_frequencies(){intid;doublefrequency;hp_globals.cpu_frequencies

=malloc(sizeof(double)*hp_globals.cpu_num);if(hp_globals.cpu_frequencies==NULL){return;}/* Iterate over all cpus found

on the machine. */for(id=0;

id

++id){/* Only get the previous cpu affinity

mask for the first call. */if(bind_to_cpu(id)){

//為了測(cè)定每個(gè)CPU核的時(shí)鐘頻率端礼,

//需要先綁定到指定的核上運(yùn)行

clear_frequencies();return;}/* Make sure the current process

gets scheduled to the target cpu.

This might not be necessary though. */usleep(0);frequency=get_cpu_frequency();if(frequency==0.0){clear_frequencies();return;}hp_globals.cpu_frequencies[id]

=frequency;}

}

在獲取了每個(gè)核的CPU時(shí)鐘頻率后禽笑,會(huì)隨機(jī)地綁定到某個(gè)核上繼續(xù)執(zhí)行入录。

最后在get_us_from_tsc()中,通過(guò)代碼塊執(zhí)行花費(fèi)的時(shí)鐘周期數(shù)/當(dāng)前CPU時(shí)鐘頻率得到代碼塊執(zhí)行的時(shí)間wt佳镜。采用這種方式能更精確地獲取wt僚稿,欲詳細(xì)了解可以去研究下micro-benchmarking= =。

四蟀伸、xhprof_disable輸出數(shù)據(jù)中cpu的實(shí)現(xiàn)

在END_PROFILING->hp_globals.mode_cb.end_fn_cb->hp_mode_hier_endfn_cb中:

if(hp_globals.xhprof_flags

&XHPROF_FLAGS_CPU){/* Get CPU usage */getrusage(RUSAGE_SELF,&ru_end);

//系統(tǒng)調(diào)用蚀同,獲取當(dāng)前進(jìn)程的資源使用情況/* Bump CPU stats in the counts hashtable */hp_inc_count(counts,"cpu",

(get_us_interval(

&(top->ru_start_hprof.ru_utime),&(ru_end.ru_utime))+get_us_interval(

&(top->ru_start_hprof.ru_stime),&(ru_end.ru_stime)))TSRMLS_CC

);}

top->ru_start_hprof是在hp_mode_hier_beginfn_cb()中通過(guò)getrusage()設(shè)置的。

ru_utime為user time啊掏,ru_stime為system time蠢络,兩者加起來(lái)就得到cpu time了。

五迟蜜、xhprof_disable輸出數(shù)據(jù)中mupmu的實(shí)現(xiàn)

在END_PROFILING->hp_globals.mode_cb.end_fn_cb->hp_mode_hier_endfn_cb中:

if(hp_globals.xhprof_flags

&XHPROF_FLAGS_MEMORY){/* Get Memory usage */mu_end=zend_memory_usage(0

TSRMLS_CC);pmu_end=zend_memory_peak_usage(0

TSRMLS_CC);/* Bump Memory stats in the counts hashtable */hp_inc_count(counts,"mu",

mu_end-top->mu_start_hprof

TSRMLS_CC);hp_inc_count(counts,"pmu",

pmu_end-top->pmu_start_hprof

TSRMLS_CC);}

top->mu_start_hprof和top->pmu_start_hprof已在BEGIN_PROFILING->hp_globals.mode_cb.begin_fn_cb->hp_mode_hier_beginfn_cb中通過(guò)zend_memory_usage和zend_memory_peak_usage賦值刹孔。這兩個(gè)zend函數(shù)的實(shí)現(xiàn):

ZEND_API

size_t

zend_memory_usage

(intreal_usageTSRMLS_DC){if(real_usage){returnAG(mm_heap)->real_size;

//PHP實(shí)際占用了的系統(tǒng)內(nèi)存

}else{size_tusage=AG(mm_heap)->size;

#if ZEND_MM_CACHE

usage-=AG(mm_heap)->cached;

#endif

returnusage;}

}

ZEND_API

size_t

zend_memory_peak_usage

(intreal_usageTSRMLS_DC){if(real_usage){returnAG(mm_heap)->real_peak;}else{returnAG(mm_heap)->peak;}

}

可見(jiàn),這里獲取的mupmu是當(dāng)前使用到的內(nèi)存娜睛,不包括已從系統(tǒng)申請(qǐng)的但未使用的芦疏。

六、由上面可發(fā)現(xiàn)各項(xiàng)統(tǒng)計(jì)信息是通過(guò)hp_inc_count進(jìn)行疊加得到的微姊。

XHPROF_MODE_SAMPLED模式分析

一酸茴、該模式不支持過(guò)濾掉不想被profile的函數(shù)

二、打點(diǎn)方式與XHPROF_MODE_HIERARCHICAL模式相同兢交,不同點(diǎn)在于BEGIN_PROFILING調(diào)用的是hp_mode_sampled_beginfn_cb薪捍,END_PROFILING調(diào)用的是hp_mode_sampled_endfn_cb,而在這兩個(gè)函數(shù)中都只調(diào)用了hp_sample_check()配喳,其代碼如下:

void

hp_sample_check

(hp_entry_t**entriesTSRMLS_DC){/* Validate input */if(!entries||!(*entries)){return;}/* See if its time to sample.

//While loop is to handle a single function ? * taking a long time and passing

several sampling intervals. */while(

(cycle_timer()-hp_globals.last_sample_tsc)>hp_globals.sampling_interval_tsc){

//如果當(dāng)前時(shí)鐘周期數(shù) - 上一次的時(shí)鐘周期數(shù)

> 采樣的時(shí)鐘周期間隔則繼續(xù)采樣/* bump last_sample_tsc */hp_globals.last_sample_tsc

+=hp_globals.sampling_interval_tsc;

//將上一次的時(shí)鐘周期數(shù)加上采樣的時(shí)鐘周期數(shù)間隔/* bump last_sample_time -

HAS TO BE UPDATED BEFORE

calling hp_sample_stack */incr_us_interval(

&hp_globals.last_sample_time,

XHPROF_SAMPLING_INTERVAL);

//更新上一次的采樣時(shí)間點(diǎn)/* sample the stack */hp_sample_stack(entriesTSRMLS_CC);

//采樣數(shù)據(jù)

}return;

}

在hp_sample_stack()中就是往hp_globals.stats_count中添加:函數(shù)調(diào)用棧 => 采樣時(shí)間點(diǎn)酪穿。

在hp_begin->hp_init_profiler_state->hp_globals.mode_cb.init_cb->hp_mode_sampled_init_cb中做了一些初始化工作:

void

hp_mode_sampled_init_cb

(TSRMLS_D){structtimevalnow;uint64truncated_us;uint64truncated_tsc;doublecpu_freq

=hp_globals.cpu_frequencies[

hp_globals.cur_cpu_id];/* Init the last_sample in tsc */hp_globals.last_sample_tsc

=cycle_timer();

//初始化開(kāi)始采樣的時(shí)鐘周期數(shù)

gettimeofday(

&hp_globals.last_sample_time

,0);

//初始化開(kāi)始采樣的時(shí)間點(diǎn)

now=hp_globals.last_sample_time;

XHPROF_SAMPLING_INTERVAL的值為0.1秒

hp_trunc_time的作用是

將hp_globals.last_sample_time更新

為XHPROF_SAMPLING_INTERVAL的整數(shù)倍

hp_trunc_time(

&hp_globals.last_sample_time,

XHPROF_SAMPLING_INTERVAL);

truncated_us=get_us_interval(

&hp_globals.last_sample_time

,&now);

//被hp_trunc_time 截?cái)嗟舻臅r(shí)間

truncated_tsc=

get_tsc_from_us(

truncated_us,cpu_freq);

if(hp_globals.last_sample_tsc

>truncated_tsc){/* just to be safe while

subtracting unsigned ints */hp_globals.last_sample_tsc

-=truncated_tsc;

//為了使last_sample_tsc

和last_sample_time保持同步}對(duì)于hp_globals.last_sample_tsc

<= truncated_tsc的情況,

出現(xiàn)的可能性非常小晴裹,

即使真的出現(xiàn)了也只是漏了第一次采樣hp_globals.sampling_interval_tsc

=get_tsc_from_us(

XHPROF_SAMPLING_INTERVAL

,cpu_freq);

}

三被济、函數(shù)調(diào)用堆棧的實(shí)現(xiàn)

對(duì)于每一個(gè)hp_entry_t(即分析點(diǎn)),都會(huì)有一個(gè)prev_hprof屬性指向上一層的分析點(diǎn)涧团,hp_get_function_stack(hp_entry_t *entry, int level, char *result_buf, size_t result_len)就是通過(guò)這個(gè)將函數(shù)調(diào)用堆棧的函數(shù)名串起來(lái)只磷,在XHPROF_MODE_SAMPLED模式下level傳參是INT_MAX,也就是說(shuō)盡可能的將整個(gè)函數(shù)調(diào)用棧的函數(shù)名串起來(lái)返回泌绣,而在XHPROF_MODE_HIERARCHICAL模式下level傳參是2钮追,也就是說(shuō)只取當(dāng)前跟其上一級(jí)的函數(shù)名串起來(lái)返回,從兩種模式的輸出結(jié)果就可以看出來(lái)了阿迈。

總結(jié)

從以上分析元媚,基本了解到了xhprof的整個(gè)實(shí)現(xiàn),也更清楚的知道xhprof的性能分析數(shù)據(jù)的含義,即使是采用XHPROF_MODE_HIERARCHICAL模式刊棕,我們也知道xhprof只是在每個(gè)函數(shù)執(zhí)行前后進(jìn)行打點(diǎn)和采樣炭晒,對(duì)性能的影響是很小的。

--------------偉大的分割線----------------

PHP飯米粒(phpfamily) 由一群靠譜的人建立甥角,愿為PHPer帶來(lái)一些值得細(xì)細(xì)品味的精神食糧腰埂!

飯米粒只發(fā)原創(chuàng)或授權(quán)發(fā)表的文章,不轉(zhuǎn)載網(wǎng)上的文章

所發(fā)的文章蜈膨,均可找到原作者進(jìn)行溝通屿笼。

也希望各位多多打賞(算作稿費(fèi)給文章作者),更希望大家多多投搞翁巍。

投稿請(qǐng)聯(lián)系:

shenzhe163@gmail.com

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末驴一,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子灶壶,更是在濱河造成了極大的恐慌肝断,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驰凛,死亡現(xiàn)場(chǎng)離奇詭異胸懈,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)恰响,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門趣钱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人胚宦,你說(shuō)我怎么就攤上這事首有。” “怎么了枢劝?”我有些...
    開(kāi)封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵井联,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我您旁,道長(zhǎng)烙常,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任鹤盒,我火速辦了婚禮蚕脏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘昨悼。我一直安慰自己蝗锥,他們只是感情好跃洛,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布率触。 她就那樣靜靜地躺著,像睡著了一般汇竭。 火紅的嫁衣襯著肌膚如雪葱蝗。 梳的紋絲不亂的頭發(fā)上穴张,一...
    開(kāi)封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音两曼,去河邊找鬼皂甘。 笑死,一個(gè)胖子當(dāng)著我的面吹牛悼凑,可吹牛的內(nèi)容都是我干的偿枕。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼户辫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼渐夸!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起渔欢,我...
    開(kāi)封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤墓塌,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后奥额,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體苫幢,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年垫挨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了韩肝。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡九榔,死狀恐怖伞梯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情帚屉,我是刑警寧澤谜诫,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站攻旦,受9級(jí)特大地震影響喻旷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜牢屋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一且预、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧烙无,春花似錦锋谐、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春三热,著一層夾襖步出監(jiān)牢的瞬間鼓择,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工就漾, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留呐能,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓抑堡,卻偏偏與公主長(zhǎng)得像摆出,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子首妖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容