提交 317ae05a 编写于 作者: B bors

Auto merge of #54325 - michaelwoerister:incr-thinlto-tests, r=alexcrichton

incr.comp.: Allow for more fine-grained testing of CGU reuse and use it to test incremental ThinLTO.

This adds some tests specifically targeted at incremental ThinLTO, plus the infrastructure for tracking the kind of cache hit/miss we had for a given CGU. @alexcrichton, let me know if you can think of any more tests to add. ThinLTO works rather reliably for small functions, so we should be able to test it in a robust way.

I think after this lands it might time for a "Help us test incremental ThinLTO" post on irlo.

r? @alexcrichton
// Copyright 2018 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Some facilities for tracking how codegen-units are reused during incremental
//! compilition. This is used for incremental compiliation tests and debug
//! output.
use session::Session;
use rustc_data_structures::fx::FxHashMap;
use std::sync::{Arc, Mutex};
use syntax_pos::Span;
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub enum CguReuse {
No,
PreLto,
PostLto,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ComparisonKind {
Exact,
AtLeast,
}
struct TrackerData {
actual_reuse: FxHashMap<String, CguReuse>,
expected_reuse: FxHashMap<String, (String, SendSpan, CguReuse, ComparisonKind)>,
}
// Span does not implement `Send`, so we can't just store it in the shared
// `TrackerData` object. Instead of splitting up `TrackerData` into shared and
// non-shared parts (which would be complicated), we just mark the `Span` here
// explicitly as `Send`. That's safe because the span data here is only ever
// accessed from the main thread.
struct SendSpan(Span);
unsafe impl Send for SendSpan {}
#[derive(Clone)]
pub struct CguReuseTracker {
data: Option<Arc<Mutex<TrackerData>>>,
}
impl CguReuseTracker {
pub fn new() -> CguReuseTracker {
let data = TrackerData {
actual_reuse: FxHashMap(),
expected_reuse: FxHashMap(),
};
CguReuseTracker {
data: Some(Arc::new(Mutex::new(data))),
}
}
pub fn new_disabled() -> CguReuseTracker {
CguReuseTracker {
data: None,
}
}
pub fn set_actual_reuse(&self, cgu_name: &str, kind: CguReuse) {
if let Some(ref data) = self.data {
debug!("set_actual_reuse({:?}, {:?})", cgu_name, kind);
let prev_reuse = data.lock()
.unwrap()
.actual_reuse
.insert(cgu_name.to_string(), kind);
if let Some(prev_reuse) = prev_reuse {
// The only time it is legal to overwrite reuse state is when
// we discover during ThinLTO that we can actually reuse the
// post-LTO version of a CGU.
assert_eq!(prev_reuse, CguReuse::PreLto);
}
}
}
pub fn set_expectation(&self,
cgu_name: &str,
cgu_user_name: &str,
error_span: Span,
expected_reuse: CguReuse,
comparison_kind: ComparisonKind) {
if let Some(ref data) = self.data {
debug!("set_expectation({:?}, {:?}, {:?})", cgu_name,
expected_reuse,
comparison_kind);
let mut data = data.lock().unwrap();
data.expected_reuse.insert(cgu_name.to_string(),
(cgu_user_name.to_string(),
SendSpan(error_span),
expected_reuse,
comparison_kind));
}
}
pub fn check_expected_reuse(&self, sess: &Session) {
if let Some(ref data) = self.data {
let data = data.lock().unwrap();
for (cgu_name, &(ref cgu_user_name,
ref error_span,
expected_reuse,
comparison_kind)) in &data.expected_reuse {
if let Some(&actual_reuse) = data.actual_reuse.get(cgu_name) {
let (error, at_least) = match comparison_kind {
ComparisonKind::Exact => {
(expected_reuse != actual_reuse, false)
}
ComparisonKind::AtLeast => {
(actual_reuse < expected_reuse, true)
}
};
if error {
let at_least = if at_least { "at least " } else { "" };
let msg = format!("CGU-reuse for `{}` is `{:?}` but \
should be {}`{:?}`",
cgu_user_name,
actual_reuse,
at_least,
expected_reuse);
sess.span_err(error_span.0, &msg);
}
} else {
let msg = format!("CGU-reuse for `{}` (mangled: `{}`) was \
not recorded",
cgu_user_name,
cgu_name);
sess.span_fatal(error_span.0, &msg);
}
}
}
}
}
......@@ -16,6 +16,7 @@
mod query;
mod safe;
mod serialized;
pub mod cgu_reuse_tracker;
pub use self::dep_tracking_map::{DepTrackingMap, DepTrackingMapConfig};
pub use self::dep_node::{DepNode, DepKind, DepConstructor, WorkProductId, label_strs};
......
......@@ -30,16 +30,7 @@
pub const ATTR_THEN_THIS_WOULD_NEED: &'static str = "rustc_then_this_would_need";
pub const ATTR_PARTITION_REUSED: &'static str = "rustc_partition_reused";
pub const ATTR_PARTITION_CODEGENED: &'static str = "rustc_partition_codegened";
pub const DEP_GRAPH_ASSERT_ATTRS: &'static [&'static str] = &[
ATTR_IF_THIS_CHANGED,
ATTR_THEN_THIS_WOULD_NEED,
ATTR_DIRTY,
ATTR_CLEAN,
ATTR_PARTITION_REUSED,
ATTR_PARTITION_CODEGENED,
];
pub const ATTR_EXPECTED_CGU_REUSE: &'static str = "rustc_expected_cgu_reuse";
pub const IGNORED_ATTRIBUTES: &'static [&'static str] = &[
"cfg",
......@@ -49,4 +40,5 @@
ATTR_CLEAN,
ATTR_PARTITION_REUSED,
ATTR_PARTITION_CODEGENED,
ATTR_EXPECTED_CGU_REUSE,
];
......@@ -11,6 +11,7 @@
pub use self::code_stats::{DataTypeKind, SizeKind, FieldInfo, VariantInfo};
use self::code_stats::CodeStats;
use dep_graph::cgu_reuse_tracker::CguReuseTracker;
use hir::def_id::CrateNum;
use rustc_data_structures::fingerprint::Fingerprint;
......@@ -124,6 +125,9 @@ pub struct Session {
pub imported_macro_spans: OneThread<RefCell<FxHashMap<Span, (String, Span)>>>,
incr_comp_session: OneThread<RefCell<IncrCompSession>>,
/// Used for incremental compilation tests. Will only be populated if
/// `-Zquery-dep-graph` is specified.
pub cgu_reuse_tracker: CguReuseTracker,
/// Used by -Z profile-queries in util::common
pub profile_channel: Lock<Option<mpsc::Sender<ProfileQueriesMsg>>>,
......@@ -1116,6 +1120,12 @@ pub fn build_session_(
};
let working_dir = file_path_mapping.map_prefix(working_dir);
let cgu_reuse_tracker = if sopts.debugging_opts.query_dep_graph {
CguReuseTracker::new()
} else {
CguReuseTracker::new_disabled()
};
let sess = Session {
target: target_cfg,
host,
......@@ -1146,6 +1156,7 @@ pub fn build_session_(
injected_panic_runtime: Once::new(),
imported_macro_spans: OneThread::new(RefCell::new(FxHashMap::default())),
incr_comp_session: OneThread::new(RefCell::new(IncrCompSession::NotInitialized)),
cgu_reuse_tracker,
self_profiling: Lock::new(SelfProfiler::new()),
profile_channel: Lock::new(None),
perf_stats: PerfStats {
......
......@@ -18,6 +18,7 @@
use llvm;
use memmap;
use rustc::dep_graph::WorkProduct;
use rustc::dep_graph::cgu_reuse_tracker::CguReuse;
use rustc::hir::def_id::LOCAL_CRATE;
use rustc::middle::exported_symbols::SymbolExportLevel;
use rustc::session::config::{self, Lto};
......@@ -538,6 +539,8 @@ fn thin_lto(cgcx: &CodegenContext,
let work_product = green_modules[module_name].clone();
copy_jobs.push(work_product);
info!(" - {}: re-used", module_name);
cgcx.cgu_reuse_tracker.set_actual_reuse(module_name,
CguReuse::PostLto);
continue
}
}
......
......@@ -21,6 +21,7 @@
use rustc_incremental::{copy_cgu_workproducts_to_incr_comp_cache_dir,
in_incr_comp_dir, in_incr_comp_dir_sess};
use rustc::dep_graph::{WorkProduct, WorkProductId, WorkProductFileKind};
use rustc::dep_graph::cgu_reuse_tracker::CguReuseTracker;
use rustc::middle::cstore::EncodedMetadata;
use rustc::session::config::{self, OutputFilenames, OutputType, Passes, Sanitizer, Lto};
use rustc::session::Session;
......@@ -377,6 +378,8 @@ pub struct CodegenContext {
// The incremental compilation session directory, or None if we are not
// compiling incrementally
pub incr_comp_session_dir: Option<PathBuf>,
// Used to update CGU re-use information during the thinlto phase.
pub cgu_reuse_tracker: CguReuseTracker,
// Channel back to the main control thread to send messages to
coordinator_send: Sender<Box<dyn Any + Send>>,
// A reference to the TimeGraph so we can register timings. None means that
......@@ -1607,6 +1610,7 @@ fn start_executing_work(tcx: TyCtxt,
remark: sess.opts.cg.remark.clone(),
worker: 0,
incr_comp_session_dir: sess.incr_comp_session_dir_opt().map(|r| r.clone()),
cgu_reuse_tracker: sess.cgu_reuse_tracker.clone(),
coordinator_send,
diag_emitter: shared_emitter.clone(),
time_graph,
......@@ -2390,6 +2394,8 @@ pub(crate) fn join(
}
};
sess.cgu_reuse_tracker.check_expected_reuse(sess);
sess.abort_if_errors();
if let Some(time_graph) = self.time_graph {
......
......@@ -32,6 +32,7 @@
use back::write::{self, OngoingCodegen};
use llvm::{self, TypeKind, get_param};
use metadata;
use rustc::dep_graph::cgu_reuse_tracker::CguReuse;
use rustc::hir::def_id::{CrateNum, DefId, LOCAL_CRATE};
use rustc::middle::lang_items::StartFnLangItem;
use rustc::middle::weak_lang_items;
......@@ -697,25 +698,18 @@ pub fn iter_globals(llmod: &'ll llvm::Module) -> ValueIter<'ll> {
}
}
#[derive(Debug)]
enum CguReUsable {
PreLto,
PostLto,
No
}
fn determine_cgu_reuse<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>,
cgu: &CodegenUnit<'tcx>)
-> CguReUsable {
-> CguReuse {
if !tcx.dep_graph.is_fully_enabled() {
return CguReUsable::No
return CguReuse::No
}
let work_product_id = &cgu.work_product_id();
if tcx.dep_graph.previous_work_product(work_product_id).is_none() {
// We don't have anything cached for this CGU. This can happen
// if the CGU did not exist in the previous session.
return CguReUsable::No
return CguReuse::No
}
// Try to mark the CGU as green. If it we can do so, it means that nothing
......@@ -732,12 +726,12 @@ fn determine_cgu_reuse<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>,
if tcx.dep_graph.try_mark_green(tcx, &dep_node).is_some() {
// We can re-use either the pre- or the post-thinlto state
if tcx.sess.lto() != Lto::No {
CguReUsable::PreLto
CguReuse::PreLto
} else {
CguReUsable::PostLto
CguReuse::PostLto
}
} else {
CguReUsable::No
CguReuse::No
}
}
......@@ -894,8 +888,11 @@ pub fn codegen_crate<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>,
ongoing_codegen.wait_for_signal_to_codegen_item();
ongoing_codegen.check_for_errors(tcx.sess);
let loaded_from_cache = match determine_cgu_reuse(tcx, &cgu) {
CguReUsable::No => {
let cgu_reuse = determine_cgu_reuse(tcx, &cgu);
tcx.sess.cgu_reuse_tracker.set_actual_reuse(&cgu.name().as_str(), cgu_reuse);
match cgu_reuse {
CguReuse::No => {
let _timing_guard = time_graph.as_ref().map(|time_graph| {
time_graph.start(write::CODEGEN_WORKER_TIMELINE,
write::CODEGEN_WORK_PACKAGE_KIND,
......@@ -907,14 +904,14 @@ pub fn codegen_crate<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>,
total_codegen_time += start_time.elapsed();
false
}
CguReUsable::PreLto => {
CguReuse::PreLto => {
write::submit_pre_lto_module_to_llvm(tcx, CachedModuleCodegen {
name: cgu.name().to_string(),
source: cgu.work_product(tcx),
});
true
}
CguReUsable::PostLto => {
CguReuse::PostLto => {
write::submit_post_lto_module_to_llvm(tcx, CachedModuleCodegen {
name: cgu.name().to_string(),
source: cgu.work_product(tcx),
......@@ -922,12 +919,6 @@ pub fn codegen_crate<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>,
true
}
};
if tcx.dep_graph.is_fully_enabled() {
let dep_node = cgu.codegen_dep_node(tcx);
let dep_node_index = tcx.dep_graph.dep_node_index_of(&dep_node);
tcx.dep_graph.mark_loaded_from_cache(dep_node_index, loaded_from_cache);
}
}
ongoing_codegen.codegen_finished(tcx);
......@@ -938,9 +929,7 @@ pub fn codegen_crate<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>,
"codegen to LLVM IR",
total_codegen_time);
if tcx.sess.opts.incremental.is_some() {
::rustc_incremental::assert_module_sources::assert_module_sources(tcx);
}
rustc_incremental::assert_module_sources::assert_module_sources(tcx);
symbol_names_test::report_symbol_names(tcx);
......
......@@ -26,19 +26,23 @@
//! The reason that we use `cfg=...` and not `#[cfg_attr]` is so that
//! the HIR doesn't change as a result of the annotations, which might
//! perturb the reuse results.
//!
//! `#![rustc_expected_cgu_reuse(module="spike", cfg="rpass2", kind="post-lto")]
//! allows for doing a more fine-grained check to see if pre- or post-lto data
//! was re-used.
use rustc::hir::def_id::LOCAL_CRATE;
use rustc::dep_graph::{DepNode, DepConstructor};
use rustc::dep_graph::cgu_reuse_tracker::*;
use rustc::mir::mono::CodegenUnitNameBuilder;
use rustc::ty::TyCtxt;
use std::collections::BTreeSet;
use syntax::ast;
use rustc::ich::{ATTR_PARTITION_REUSED, ATTR_PARTITION_CODEGENED};
use rustc::ich::{ATTR_PARTITION_REUSED, ATTR_PARTITION_CODEGENED,
ATTR_EXPECTED_CGU_REUSE};
const MODULE: &'static str = "module";
const CFG: &'static str = "cfg";
#[derive(Debug, PartialEq, Clone, Copy)]
enum Disposition { Reused, Codegened }
const KIND: &'static str = "kind";
pub fn assert_module_sources<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>) {
tcx.dep_graph.with_ignore(|| {
......@@ -46,7 +50,18 @@ pub fn assert_module_sources<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>) {
return;
}
let ams = AssertModuleSource { tcx };
let available_cgus = tcx
.collect_and_partition_mono_items(LOCAL_CRATE)
.1
.iter()
.map(|cgu| format!("{}", cgu.name()))
.collect::<BTreeSet<String>>();
let ams = AssertModuleSource {
tcx,
available_cgus
};
for attr in &tcx.hir.krate().attrs {
ams.check_attr(attr);
}
......@@ -54,19 +69,39 @@ pub fn assert_module_sources<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>) {
}
struct AssertModuleSource<'a, 'tcx: 'a> {
tcx: TyCtxt<'a, 'tcx, 'tcx>
tcx: TyCtxt<'a, 'tcx, 'tcx>,
available_cgus: BTreeSet<String>,
}
impl<'a, 'tcx> AssertModuleSource<'a, 'tcx> {
fn check_attr(&self, attr: &ast::Attribute) {
let disposition = if attr.check_name(ATTR_PARTITION_REUSED) {
Disposition::Reused
let (expected_reuse, comp_kind) = if attr.check_name(ATTR_PARTITION_REUSED) {
(CguReuse::PreLto, ComparisonKind::AtLeast)
} else if attr.check_name(ATTR_PARTITION_CODEGENED) {
Disposition::Codegened
(CguReuse::No, ComparisonKind::Exact)
} else if attr.check_name(ATTR_EXPECTED_CGU_REUSE) {
match &self.field(attr, KIND).as_str()[..] {
"no" => (CguReuse::No, ComparisonKind::Exact),
"pre-lto" => (CguReuse::PreLto, ComparisonKind::Exact),
"post-lto" => (CguReuse::PostLto, ComparisonKind::Exact),
"any" => (CguReuse::PreLto, ComparisonKind::AtLeast),
other => {
self.tcx.sess.span_fatal(
attr.span,
&format!("unknown cgu-reuse-kind `{}` specified", other));
}
}
} else {
return;
};
if !self.tcx.sess.opts.debugging_opts.query_dep_graph {
self.tcx.sess.span_fatal(
attr.span,
&format!("found CGU-reuse attribute but `-Zquery-dep-graph` \
was not specified"));
}
if !self.check_config(attr) {
debug!("check_attr: config does not match, ignoring attr");
return;
......@@ -101,43 +136,24 @@ fn check_attr(&self, attr: &ast::Attribute) {
debug!("mapping '{}' to cgu name '{}'", self.field(attr, MODULE), cgu_name);
let dep_node = DepNode::new(self.tcx,
DepConstructor::CompileCodegenUnit(cgu_name));
if let Some(loaded_from_cache) = self.tcx.dep_graph.was_loaded_from_cache(&dep_node) {
match (disposition, loaded_from_cache) {
(Disposition::Reused, false) => {
self.tcx.sess.span_err(
attr.span,
&format!("expected module named `{}` to be Reused but is Codegened",
user_path));
}
(Disposition::Codegened, true) => {
self.tcx.sess.span_err(
attr.span,
&format!("expected module named `{}` to be Codegened but is Reused",
user_path));
}
(Disposition::Reused, true) |
(Disposition::Codegened, false) => {
// These are what we would expect.
}
}
} else {
let available_cgus = self.tcx
.collect_and_partition_mono_items(LOCAL_CRATE)
.1
.iter()
.map(|cgu| format!("{}", cgu.name()))
.collect::<Vec<String>>()
.join(", ");
if !self.available_cgus.contains(&cgu_name.as_str()[..]) {
self.tcx.sess.span_err(attr.span,
&format!("no module named `{}` (mangled: {}).\nAvailable modules: {}",
&format!("no module named `{}` (mangled: {}). \
Available modules: {}",
user_path,
cgu_name,
available_cgus));
self.available_cgus
.iter()
.cloned()
.collect::<Vec<_>>()
.join(", ")));
}
self.tcx.sess.cgu_reuse_tracker.set_expectation(&cgu_name.as_str(),
&user_path,
attr.span,
expected_reuse,
comp_kind);
}
fn field(&self, attr: &ast::Attribute, name: &str) -> ast::Name {
......@@ -171,5 +187,4 @@ fn check_config(&self, attr: &ast::Attribute) -> bool {
debug!("check_config: no match found");
return false;
}
}
......@@ -933,6 +933,12 @@ pub fn is_builtin_attr(attr: &ast::Attribute) -> bool {
is just used for rustc unit tests \
and will never be stable",
cfg_fn!(rustc_attrs))),
("rustc_expected_cgu_reuse", Whitelisted, Gated(Stability::Unstable,
"rustc_attrs",
"this attribute \
is just used for rustc unit tests \
and will never be stable",
cfg_fn!(rustc_attrs))),
("rustc_synthetic", Whitelisted, Gated(Stability::Unstable,
"rustc_attrs",
"this attribute \
......
......@@ -13,6 +13,7 @@
// revisions:rpass1 rpass2
// aux-build:a.rs
// compile-flags: -Zquery-dep-graph
#![feature(rustc_attrs)]
#![crate_type = "bin"]
......
// Copyright 2018 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
// This test checks that the LTO phase is re-done for CGUs that import something
// via ThinLTO and that imported thing changes while the definition of the CGU
// stays untouched.
// revisions: cfail1 cfail2 cfail3
// compile-flags: -Z query-dep-graph -O
// compile-pass
#![feature(rustc_attrs)]
#![crate_type="rlib"]
#![rustc_expected_cgu_reuse(module="cgu_invalidated_via_import-foo",
cfg="cfail2",
kind="no")]
#![rustc_expected_cgu_reuse(module="cgu_invalidated_via_import-foo",
cfg="cfail3",
kind="post-lto")]
#![rustc_expected_cgu_reuse(module="cgu_invalidated_via_import-bar",
cfg="cfail2",
kind="pre-lto")]
#![rustc_expected_cgu_reuse(module="cgu_invalidated_via_import-bar",
cfg="cfail3",
kind="post-lto")]
mod foo {
// Trivial functions like this one are imported very reliably by ThinLTO.
#[cfg(cfail1)]
pub fn inlined_fn() -> u32 {
1234
}
#[cfg(not(cfail1))]
pub fn inlined_fn() -> u32 {
1234
}
}
pub mod bar {
use foo::inlined_fn;
pub fn caller() -> u32 {
inlined_fn()
}
}
// Copyright 2018 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
// This test checks that a change in a CGU does not invalidate an unrelated CGU
// during incremental ThinLTO.
// revisions: cfail1 cfail2 cfail3
// compile-flags: -Z query-dep-graph -O
// compile-pass
#![feature(rustc_attrs)]
#![crate_type="rlib"]
#![rustc_expected_cgu_reuse(module="independent_cgus_dont_affect_each_other-foo",
cfg="cfail2",
kind="no")]
#![rustc_expected_cgu_reuse(module="independent_cgus_dont_affect_each_other-foo",
cfg="cfail3",
kind="post-lto")]
#![rustc_expected_cgu_reuse(module="independent_cgus_dont_affect_each_other-bar",
cfg="cfail2",
kind="pre-lto")]
#![rustc_expected_cgu_reuse(module="independent_cgus_dont_affect_each_other-bar",
cfg="cfail3",
kind="post-lto")]
#![rustc_expected_cgu_reuse(module="independent_cgus_dont_affect_each_other-baz",
cfg="cfail2",
kind="post-lto")]
#![rustc_expected_cgu_reuse(module="independent_cgus_dont_affect_each_other-baz",
cfg="cfail3",
kind="post-lto")]
mod foo {
#[cfg(cfail1)]
pub fn inlined_fn() -> u32 {
1234
}
#[cfg(not(cfail1))]
pub fn inlined_fn() -> u32 {
1234
}
}
pub mod bar {
use foo::inlined_fn;
pub fn caller() -> u32 {
inlined_fn()
}
}
pub mod baz {
pub fn unrelated_to_other_fns() -> u64 {
0xbeef
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册