mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
[tracing] Add convenience macro for function tracing (#36573)
Adds a convenience macro that adds sane defaults to the tracing macro. Closes: #36534 Testing: - Pass in sample test cases and compare against expected behaviour. --------- Signed-off-by: Astraea Quinn Skoutelli <astraea.quinn.skoutelli@huawei.com>
This commit is contained in:
parent
a57ab6deca
commit
b6967fc4c8
7 changed files with 431 additions and 4 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -4321,6 +4321,7 @@ dependencies = [
|
||||||
"servo-media",
|
"servo-media",
|
||||||
"servo-media-dummy",
|
"servo-media-dummy",
|
||||||
"servo-media-gstreamer",
|
"servo-media-gstreamer",
|
||||||
|
"servo-tracing",
|
||||||
"servo_allocator",
|
"servo_allocator",
|
||||||
"servo_config",
|
"servo_config",
|
||||||
"servo_geometry",
|
"servo_geometry",
|
||||||
|
@ -6734,6 +6735,16 @@ dependencies = [
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "servo-tracing"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "servo_allocator"
|
name = "servo_allocator"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
@ -6891,6 +6902,7 @@ dependencies = [
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"rustls",
|
"rustls",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"servo-tracing",
|
||||||
"servo_allocator",
|
"servo_allocator",
|
||||||
"shellwords",
|
"shellwords",
|
||||||
"sig",
|
"sig",
|
||||||
|
|
|
@ -125,6 +125,7 @@ serde_json = "1.0"
|
||||||
servo-media = { git = "https://github.com/servo/media" }
|
servo-media = { git = "https://github.com/servo/media" }
|
||||||
servo-media-dummy = { git = "https://github.com/servo/media" }
|
servo-media-dummy = { git = "https://github.com/servo/media" }
|
||||||
servo-media-gstreamer = { git = "https://github.com/servo/media" }
|
servo-media-gstreamer = { git = "https://github.com/servo/media" }
|
||||||
|
servo-tracing = { path = "components/servo_tracing" }
|
||||||
servo_arc = { git = "https://github.com/servo/stylo", branch = "2025-03-15" }
|
servo_arc = { git = "https://github.com/servo/stylo", branch = "2025-03-15" }
|
||||||
smallbitvec = "2.6.0"
|
smallbitvec = "2.6.0"
|
||||||
smallvec = "1.15"
|
smallvec = "1.15"
|
||||||
|
|
|
@ -104,6 +104,7 @@ serde = { workspace = true }
|
||||||
servo-media = { workspace = true }
|
servo-media = { workspace = true }
|
||||||
servo-media-dummy = { workspace = true }
|
servo-media-dummy = { workspace = true }
|
||||||
servo-media-gstreamer = { workspace = true, optional = true }
|
servo-media-gstreamer = { workspace = true, optional = true }
|
||||||
|
servo-tracing = { workspace = true }
|
||||||
servo_allocator = { path = "../allocator" }
|
servo_allocator = { path = "../allocator" }
|
||||||
servo_config = { path = "../config" }
|
servo_config = { path = "../config" }
|
||||||
servo_geometry = { path = "../geometry" }
|
servo_geometry = { path = "../geometry" }
|
||||||
|
|
|
@ -246,10 +246,7 @@ impl webrender_api::RenderNotifier for RenderNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Servo {
|
impl Servo {
|
||||||
#[cfg_attr(
|
#[servo_tracing::instrument(skip(builder))]
|
||||||
feature = "tracing",
|
|
||||||
tracing::instrument(skip(builder), fields(servo_profiling = true), level = "trace",)
|
|
||||||
)]
|
|
||||||
fn new(builder: ServoBuilder) -> Self {
|
fn new(builder: ServoBuilder) -> Self {
|
||||||
// Global configuration options, parsed from the command line.
|
// Global configuration options, parsed from the command line.
|
||||||
let opts = builder.opts.map(|opts| *opts);
|
let opts = builder.opts.map(|opts| *opts);
|
||||||
|
|
21
components/servo_tracing/Cargo.toml
Normal file
21
components/servo_tracing/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "servo-tracing"
|
||||||
|
edition.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = { workspace = true }
|
||||||
|
proc-macro2 = { workspace = true }
|
||||||
|
syn = { version = "2", features = ["full"] }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "lib.rs"
|
||||||
|
proc-macro = true
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
prettyplease = "0.2.32"
|
394
components/servo_tracing/lib.rs
Normal file
394
components/servo_tracing/lib.rs
Normal file
|
@ -0,0 +1,394 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::Punct;
|
||||||
|
use quote::{ToTokens, TokenStreamExt, quote};
|
||||||
|
use syn::parse::{Parse, Parser};
|
||||||
|
use syn::punctuated::Punctuated;
|
||||||
|
use syn::token::Comma;
|
||||||
|
use syn::{Expr, ItemFn, Meta, MetaList, Token, parse_quote, parse2};
|
||||||
|
|
||||||
|
struct Fields(MetaList);
|
||||||
|
impl From<MetaList> for Fields {
|
||||||
|
fn from(value: MetaList) -> Self {
|
||||||
|
Fields(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fields {
|
||||||
|
fn create_with_servo_profiling() -> Self {
|
||||||
|
Fields(parse_quote! { fields(servo_profiling = true) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_servo_profiling(&mut self) -> syn::Result<()> {
|
||||||
|
let metalist = std::mem::replace(&mut self.0, parse_quote! {field()});
|
||||||
|
|
||||||
|
let arguments: Punctuated<Meta, Comma> =
|
||||||
|
Punctuated::parse_terminated.parse2(metalist.tokens)?;
|
||||||
|
|
||||||
|
let servo_profile_given = arguments
|
||||||
|
.iter()
|
||||||
|
.any(|arg| arg.path().is_ident("servo_profiling"));
|
||||||
|
|
||||||
|
let metalist = if servo_profile_given {
|
||||||
|
parse_quote! {
|
||||||
|
fields(#arguments)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parse_quote! {
|
||||||
|
fields(servo_profiling=true, #arguments)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = std::mem::replace(&mut self.0, metalist);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for Fields {
|
||||||
|
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||||
|
let items = &self.0;
|
||||||
|
tokens.append_all(quote! { #items });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enum Directive {
|
||||||
|
Passthrough(Meta),
|
||||||
|
Level(Expr),
|
||||||
|
Fields(Fields),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Fields> for Directive {
|
||||||
|
fn from(value: Fields) -> Self {
|
||||||
|
Directive::Fields(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Directive {
|
||||||
|
fn is_level(&self) -> bool {
|
||||||
|
matches!(self, Directive::Level(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fields_mut(&mut self) -> Option<&mut Fields> {
|
||||||
|
match self {
|
||||||
|
Directive::Fields(fields) => Some(fields),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for Directive {
|
||||||
|
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||||
|
match self {
|
||||||
|
Directive::Passthrough(meta) => tokens.append_all(quote! { #meta }),
|
||||||
|
Directive::Level(level) => tokens.append_all(quote! { level = #level }),
|
||||||
|
Directive::Fields(fields) => tokens.append_all(quote! { #fields }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for InstrumentConfiguration {
|
||||||
|
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||||
|
tokens.append_terminated(&self.0, Punct::new(',', proc_macro2::Spacing::Joint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InstrumentConfiguration(Vec<Directive>);
|
||||||
|
|
||||||
|
impl InstrumentConfiguration {
|
||||||
|
fn inject_servo_profiling(&mut self) -> syn::Result<()> {
|
||||||
|
let fields = self.0.iter_mut().find_map(Directive::fields_mut);
|
||||||
|
match fields {
|
||||||
|
None => {
|
||||||
|
self.0
|
||||||
|
.push(Directive::from(Fields::create_with_servo_profiling()));
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
Some(fields) => fields.inject_servo_profiling(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_level(&mut self) {
|
||||||
|
if self.0.iter().any(|a| a.is_level()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.0.push(Directive::Level(parse_quote! { "trace" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for InstrumentConfiguration {
|
||||||
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
let args = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
|
||||||
|
let mut components = vec![];
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
match arg {
|
||||||
|
Meta::List(meta_list) if meta_list.path.is_ident("fields") => {
|
||||||
|
components.push(Directive::Fields(meta_list.into()));
|
||||||
|
},
|
||||||
|
Meta::NameValue(meta_name_value) if meta_name_value.path.is_ident("level") => {
|
||||||
|
components.push(Directive::Level(meta_name_value.value));
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
components.push(Directive::Passthrough(arg));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(InstrumentConfiguration(components))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instrument_internal(
|
||||||
|
attr: proc_macro2::TokenStream,
|
||||||
|
item: proc_macro2::TokenStream,
|
||||||
|
) -> syn::Result<proc_macro2::TokenStream> {
|
||||||
|
// Prepare passthrough arguments for tracing::instrument
|
||||||
|
let mut configuration: InstrumentConfiguration = parse2(attr)?;
|
||||||
|
let input_fn: ItemFn = parse2(item)?;
|
||||||
|
|
||||||
|
configuration.inject_servo_profiling()?;
|
||||||
|
configuration.inject_level();
|
||||||
|
|
||||||
|
let output = quote! {
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "tracing",
|
||||||
|
tracing::instrument(
|
||||||
|
#configuration
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#input_fn
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
/// Instruments a function with some sane defaults by automatically:
|
||||||
|
/// - setting the attribute behind the "tracing" flag
|
||||||
|
/// - adding `servo_profiling = true` in the `tracing::instrument(fields(...))` argument.
|
||||||
|
/// - setting `level = "trace"` if it is not given.
|
||||||
|
///
|
||||||
|
/// This macro assumes the consuming crate has a `tracing` feature flag.
|
||||||
|
///
|
||||||
|
/// We need to be able to set the following
|
||||||
|
/// ```
|
||||||
|
/// #[cfg_attr(
|
||||||
|
/// feature = "tracing",
|
||||||
|
/// tracing::instrument(
|
||||||
|
/// name = "MyCustomName",
|
||||||
|
/// skip_all,
|
||||||
|
/// fields(servo_profiling = true),
|
||||||
|
/// level = "trace",
|
||||||
|
/// )
|
||||||
|
/// )]
|
||||||
|
/// fn my_fn() { /* .... */ }
|
||||||
|
/// ```
|
||||||
|
/// from a simpler macro, such as:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[servo_tracing::instrument(name = "MyCustomName", skip_all)]
|
||||||
|
/// fn my_fn() { /* .... */ }
|
||||||
|
/// ```
|
||||||
|
pub fn instrument(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
match instrument_internal(attr.into(), item.into()) {
|
||||||
|
Ok(stream) => stream.into(),
|
||||||
|
Err(err) => err.to_compile_error().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{ToTokens, quote};
|
||||||
|
use syn::{Attribute, ItemFn};
|
||||||
|
|
||||||
|
use crate::instrument_internal;
|
||||||
|
|
||||||
|
fn extract_instrument_attribute(item_fn: &mut ItemFn) -> TokenStream {
|
||||||
|
let attr: &Attribute = item_fn
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.find(|attr| {
|
||||||
|
// because this is a very nested structure, it is easier to check
|
||||||
|
// by constructing the full path, and then doing a string comparison.
|
||||||
|
let p = attr.path().to_token_stream().to_string();
|
||||||
|
p == "servo_tracing :: instrument"
|
||||||
|
})
|
||||||
|
.expect("Attribute `servo_tracing::instrument` not found");
|
||||||
|
|
||||||
|
// we create a tokenstream of the actual internal contents of the attribute
|
||||||
|
let attr_args = attr
|
||||||
|
.parse_args::<TokenStream>()
|
||||||
|
.expect("Failed to parse attribute args");
|
||||||
|
|
||||||
|
// we remove the tracing attribute, this is to avoid passing it as an actual attribute to itself.
|
||||||
|
item_fn.attrs.retain(|attr| {
|
||||||
|
attr.path().to_token_stream().to_string() != "servo_tracing :: instrument"
|
||||||
|
});
|
||||||
|
|
||||||
|
attr_args
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To make test case generation easy, we parse a test_case as a function item
|
||||||
|
/// with its own attributes, including [`servo_tracing::instrument`].
|
||||||
|
///
|
||||||
|
/// We extract the [`servo_tracing::instrument`] attribute, and pass it as the first argument to
|
||||||
|
/// [`servo_tracing::instrument_internal`],
|
||||||
|
fn evaluate(function: TokenStream, test_case: TokenStream, expected: TokenStream) {
|
||||||
|
let test_case = quote! {
|
||||||
|
#test_case
|
||||||
|
#function
|
||||||
|
};
|
||||||
|
let expected = quote! {
|
||||||
|
#expected
|
||||||
|
#function
|
||||||
|
};
|
||||||
|
let function_str = function.to_string();
|
||||||
|
let function_str = syn::parse_file(&function_str).expect("function to have valid syntax");
|
||||||
|
let function_str = prettyplease::unparse(&function_str);
|
||||||
|
|
||||||
|
let mut item_fn: ItemFn =
|
||||||
|
syn::parse2(test_case).expect("Failed to parse input as function");
|
||||||
|
|
||||||
|
let attr_args = extract_instrument_attribute(&mut item_fn);
|
||||||
|
let item_fn = item_fn.to_token_stream();
|
||||||
|
|
||||||
|
let generated = instrument_internal(attr_args, item_fn).expect("Generation to not fail.");
|
||||||
|
|
||||||
|
let generated = syn::parse_file(generated.to_string().as_str())
|
||||||
|
.expect("to have generated a valid function");
|
||||||
|
let generated = prettyplease::unparse(&generated);
|
||||||
|
let expected = syn::parse_file(expected.to_string().as_str())
|
||||||
|
.expect("to have been given a valid expected function");
|
||||||
|
let expected = prettyplease::unparse(&expected);
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"Generated:---------:\n{}--------\nExpected:----------\n{}",
|
||||||
|
&generated, &expected
|
||||||
|
);
|
||||||
|
assert_eq!(generated, expected);
|
||||||
|
assert!(
|
||||||
|
generated.contains(&function_str),
|
||||||
|
"Expected generated code: {generated} to contain the function code: {function_str}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn function1() -> TokenStream {
|
||||||
|
quote! {
|
||||||
|
pub fn start(
|
||||||
|
state: (),
|
||||||
|
layout_factory: (),
|
||||||
|
random_pipeline_closure_probability: (),
|
||||||
|
random_pipeline_closure_seed: (),
|
||||||
|
hard_fail: (),
|
||||||
|
canvas_create_sender: (),
|
||||||
|
canvas_ipc_sender: (),
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn function2() -> TokenStream {
|
||||||
|
quote! {
|
||||||
|
fn layout(
|
||||||
|
mut self,
|
||||||
|
layout_context: &LayoutContext,
|
||||||
|
positioning_context: &mut PositioningContext,
|
||||||
|
containing_block_for_children: &ContainingBlock,
|
||||||
|
containing_block_for_table: &ContainingBlock,
|
||||||
|
depends_on_block_constraints: bool,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn passing_servo_profiling_and_level_and_aux() {
|
||||||
|
let function = function1();
|
||||||
|
let expected = quote! {
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "tracing",
|
||||||
|
tracing::instrument(skip(state, layout_factory), fields(servo_profiling = true), level = "trace",)
|
||||||
|
)]
|
||||||
|
};
|
||||||
|
|
||||||
|
let test_case = quote! {
|
||||||
|
#[servo_tracing::instrument(skip(state, layout_factory),fields(servo_profiling = true),level = "trace",)]
|
||||||
|
};
|
||||||
|
|
||||||
|
evaluate(function, test_case, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn passing_servo_profiling_and_level() {
|
||||||
|
let function = function1();
|
||||||
|
let expected = quote! {
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "tracing",
|
||||||
|
tracing::instrument( fields(servo_profiling = true), level = "trace",)
|
||||||
|
)]
|
||||||
|
};
|
||||||
|
|
||||||
|
let test_case = quote! {
|
||||||
|
#[servo_tracing::instrument(fields(servo_profiling = true),level = "trace",)]
|
||||||
|
};
|
||||||
|
evaluate(function, test_case, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn passing_servo_profiling() {
|
||||||
|
let function = function1();
|
||||||
|
let expected = quote! {
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "tracing",
|
||||||
|
tracing::instrument( fields(servo_profiling = true), level = "trace",)
|
||||||
|
)]
|
||||||
|
};
|
||||||
|
|
||||||
|
let test_case = quote! {
|
||||||
|
#[servo_tracing::instrument(fields(servo_profiling = true))]
|
||||||
|
};
|
||||||
|
evaluate(function, test_case, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inject_level_and_servo_profiling() {
|
||||||
|
let function = function1();
|
||||||
|
let expected = quote! {
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "tracing",
|
||||||
|
tracing::instrument(fields(servo_profiling = true), level = "trace",)
|
||||||
|
)]
|
||||||
|
};
|
||||||
|
|
||||||
|
let test_case = quote! {
|
||||||
|
#[servo_tracing::instrument()]
|
||||||
|
};
|
||||||
|
evaluate(function, test_case, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instrument_with_name() {
|
||||||
|
let function = function2();
|
||||||
|
let expected = quote! {
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "tracing",
|
||||||
|
tracing::instrument(
|
||||||
|
name = "Table::layout",
|
||||||
|
skip_all,
|
||||||
|
fields(servo_profiling = true),
|
||||||
|
level = "trace",
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
};
|
||||||
|
|
||||||
|
let test_case = quote! {
|
||||||
|
#[servo_tracing::instrument(name="Table::layout", skip_all)]
|
||||||
|
};
|
||||||
|
|
||||||
|
evaluate(function, test_case, expected);
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,6 +68,7 @@ mime_guess = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
raw-window-handle = { workspace = true }
|
raw-window-handle = { workspace = true }
|
||||||
rustls = { workspace = true, features = ["aws-lc-rs"] }
|
rustls = { workspace = true, features = ["aws-lc-rs"] }
|
||||||
|
servo-tracing = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tracing = { workspace = true, optional = true }
|
tracing = { workspace = true, optional = true }
|
||||||
tracing-subscriber = { workspace = true, optional = true, features = ["env-filter"] }
|
tracing-subscriber = { workspace = true, optional = true, features = ["env-filter"] }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue